Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
d6c9678
1
Parent(s): ab8a8e6
Initial commit for HF Space
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +8 -0
- .gitignore +13 -0
- Dockerfile +47 -0
- README.md +68 -0
- backend/app/__init__.py +1 -0
- backend/app/api/__init__.py +1 -0
- backend/app/api/elaboracao.py +341 -0
- backend/app/api/health.py +11 -0
- backend/app/api/session.py +20 -0
- backend/app/api/visualizacao.py +106 -0
- backend/app/core/dados/EixosLogradouros.cpg +1 -0
- backend/app/core/dados/EixosLogradouros.dbf +3 -0
- backend/app/core/dados/EixosLogradouros.prj +1 -0
- backend/app/core/dados/EixosLogradouros.shp +3 -0
- backend/app/core/dados/EixosLogradouros.shp.xml +2 -0
- backend/app/core/dados/EixosLogradouros.shx +3 -0
- backend/app/core/elaboracao/__init__.py +0 -0
- backend/app/core/elaboracao/app.py +1910 -0
- backend/app/core/elaboracao/avaliadores.json +74 -0
- backend/app/core/elaboracao/carregamento.py +623 -0
- backend/app/core/elaboracao/charts.py +745 -0
- backend/app/core/elaboracao/core.py +2077 -0
- backend/app/core/elaboracao/formatadores.py +764 -0
- backend/app/core/elaboracao/geocodificacao.py +458 -0
- backend/app/core/elaboracao/modelo.py +991 -0
- backend/app/core/elaboracao/outliers.py +301 -0
- backend/app/core/visualizacao/__init__.py +0 -0
- backend/app/core/visualizacao/app.py +1477 -0
- backend/app/main.py +46 -0
- backend/app/models/__init__.py +1 -0
- backend/app/models/session.py +64 -0
- backend/app/services/__init__.py +1 -0
- backend/app/services/elaboracao_service.py +1124 -0
- backend/app/services/serializers.py +89 -0
- backend/app/services/session_store.py +37 -0
- backend/app/services/visualizacao_service.py +380 -0
- backend/requirements.txt +16 -0
- backend/run_backend.sh +4 -0
- frontend/index.html +12 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +21 -0
- frontend/public/logo_mesa.png +3 -0
- frontend/src/App.jsx +79 -0
- frontend/src/api.js +165 -0
- frontend/src/components/DataTable.jsx +33 -0
- frontend/src/components/ElaboracaoTab.jsx +1331 -0
- frontend/src/components/MapFrame.jsx +16 -0
- frontend/src/components/PlotFigure.jsx +48 -0
- frontend/src/components/SectionBlock.jsx +23 -0
- frontend/src/components/VisualizacaoTab.jsx +351 -0
.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
**/.DS_Store
|
| 3 |
+
**/__pycache__
|
| 4 |
+
**/.pytest_cache
|
| 5 |
+
**/.venv
|
| 6 |
+
frontend/node_modules
|
| 7 |
+
frontend/dist
|
| 8 |
+
backend/.venv
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
.venv/
|
| 7 |
+
|
| 8 |
+
# Node
|
| 9 |
+
node_modules/
|
| 10 |
+
frontend/dist/
|
| 11 |
+
|
| 12 |
+
# System
|
| 13 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-bookworm-slim AS frontend-builder
|
| 2 |
+
|
| 3 |
+
WORKDIR /src/frontend
|
| 4 |
+
|
| 5 |
+
COPY frontend/package*.json ./
|
| 6 |
+
RUN npm ci
|
| 7 |
+
|
| 8 |
+
COPY frontend/ ./
|
| 9 |
+
ARG VITE_API_BASE=.
|
| 10 |
+
ENV VITE_API_BASE=${VITE_API_BASE}
|
| 11 |
+
RUN npm run build
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
FROM python:3.11-slim
|
| 15 |
+
|
| 16 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 17 |
+
PYTHONUNBUFFERED=1
|
| 18 |
+
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends \
|
| 21 |
+
build-essential \
|
| 22 |
+
gdal-bin \
|
| 23 |
+
libgdal-dev \
|
| 24 |
+
libgeos-dev \
|
| 25 |
+
libproj-dev \
|
| 26 |
+
proj-bin \
|
| 27 |
+
proj-data \
|
| 28 |
+
libspatialindex-dev && \
|
| 29 |
+
rm -rf /var/lib/apt/lists/*
|
| 30 |
+
|
| 31 |
+
WORKDIR /app/backend
|
| 32 |
+
|
| 33 |
+
COPY backend/requirements.txt ./
|
| 34 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 35 |
+
pip install --no-cache-dir -r requirements.txt
|
| 36 |
+
|
| 37 |
+
COPY backend/ ./
|
| 38 |
+
COPY --from=frontend-builder /src/frontend/dist /app/frontend/dist
|
| 39 |
+
|
| 40 |
+
RUN useradd -m -u 1000 user && \
|
| 41 |
+
chown -R user:user /app
|
| 42 |
+
|
| 43 |
+
USER user
|
| 44 |
+
|
| 45 |
+
EXPOSE 7860
|
| 46 |
+
|
| 47 |
+
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
|
README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
---
|
|
|
|
| 2 |
title: Mesa React
|
| 3 |
emoji: 🌖
|
| 4 |
colorFrom: purple
|
|
@@ -8,3 +9,70 @@ pinned: false
|
|
| 8 |
---
|
| 9 |
|
| 10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
<<<<<<< HEAD
|
| 3 |
title: Mesa React
|
| 4 |
emoji: 🌖
|
| 5 |
colorFrom: purple
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 12 |
+
=======
|
| 13 |
+
title: mesa-frame
|
| 14 |
+
sdk: docker
|
| 15 |
+
app_port: 7860
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
# MESA Frame (FastAPI + React)
|
| 19 |
+
|
| 20 |
+
Rearquitetura do app MESA com:
|
| 21 |
+
|
| 22 |
+
- `backend/` em FastAPI
|
| 23 |
+
- `frontend/` em React (Vite)
|
| 24 |
+
- Reuso do core estatistico e de negocio original (elaboracao + visualizacao)
|
| 25 |
+
|
| 26 |
+
## Estrutura
|
| 27 |
+
|
| 28 |
+
- `backend/app/main.py`: inicializacao da API
|
| 29 |
+
- `backend/app/api/`: rotas de sessao, elaboracao e visualizacao
|
| 30 |
+
- `backend/app/services/`: orquestracao dos fluxos
|
| 31 |
+
- `backend/app/core/elaboracao`: core de elaboracao reaproveitado
|
| 32 |
+
- `backend/app/core/visualizacao`: core de visualizacao reaproveitado
|
| 33 |
+
- `frontend/src`: interface React
|
| 34 |
+
|
| 35 |
+
## Backend
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
cd backend
|
| 39 |
+
python -m venv .venv
|
| 40 |
+
source .venv/bin/activate
|
| 41 |
+
pip install -r requirements.txt
|
| 42 |
+
./run_backend.sh
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
API: `http://localhost:8000`
|
| 46 |
+
Swagger: `http://localhost:8000/docs`
|
| 47 |
+
|
| 48 |
+
## Frontend
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
cd frontend
|
| 52 |
+
npm install
|
| 53 |
+
npm run dev
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
Frontend: `http://localhost:5173`
|
| 57 |
+
|
| 58 |
+
Para apontar para outro backend:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
VITE_API_BASE=http://localhost:8000 npm run dev
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
## Funcionalidades cobertas
|
| 65 |
+
|
| 66 |
+
- Upload CSV/Excel/.dai
|
| 67 |
+
- Selecao de aba Excel
|
| 68 |
+
- Mapeamento manual de coordenadas
|
| 69 |
+
- Geocodificacao e correcoes
|
| 70 |
+
- Selecao de variaveis Y/X
|
| 71 |
+
- Busca/adocao de transformacoes
|
| 72 |
+
- Ajuste de modelo OLS e diagnosticos
|
| 73 |
+
- Filtros, iteracao e historico de outliers
|
| 74 |
+
- Avaliacao individual e exportacao de avaliacoes
|
| 75 |
+
- Exportacao de modelo `.dai` e base `.csv`
|
| 76 |
+
- Visualizacao completa de modelo salvo `.dai`
|
| 77 |
+
- Avaliacao individual na aba de visualizacao
|
| 78 |
+
>>>>>>> 6e64df9 (Initial commit for HF Space)
|
backend/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# App package
|
backend/app/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# API routers package
|
backend/app/api/elaboracao.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, File, Form, UploadFile
|
| 7 |
+
from fastapi.responses import FileResponse
|
| 8 |
+
from pydantic import BaseModel, Field
|
| 9 |
+
|
| 10 |
+
from app.services import elaboracao_service
|
| 11 |
+
from app.services.session_store import session_store
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/api/elaboracao", tags=["elaboracao"])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class SessionPayload(BaseModel):
|
| 18 |
+
session_id: str
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ConfirmSheetPayload(SessionPayload):
|
| 22 |
+
sheet_name: str
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class MapCoordsPayload(SessionPayload):
|
| 26 |
+
col_lat: str
|
| 27 |
+
col_lon: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class GeocodePayload(SessionPayload):
|
| 31 |
+
col_cdlog: str
|
| 32 |
+
col_num: str
|
| 33 |
+
auto_200: bool = False
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class CorrecaoGeo(BaseModel):
|
| 37 |
+
linha: int
|
| 38 |
+
numero_corrigido: str | None = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class GeocodeCorrecaoPayload(SessionPayload):
|
| 42 |
+
correcoes: list[CorrecaoGeo] = Field(default_factory=list)
|
| 43 |
+
auto_200: bool = False
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class ApplySelectionPayload(SessionPayload):
|
| 47 |
+
coluna_y: str
|
| 48 |
+
colunas_x: list[str]
|
| 49 |
+
dicotomicas: list[str] = Field(default_factory=list)
|
| 50 |
+
codigo_alocado: list[str] = Field(default_factory=list)
|
| 51 |
+
percentuais: list[str] = Field(default_factory=list)
|
| 52 |
+
outliers_anteriores: list[int] = Field(default_factory=list)
|
| 53 |
+
grau_min_coef: int = 1
|
| 54 |
+
grau_min_f: int = 0
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class ClassificarXPayload(SessionPayload):
|
| 58 |
+
colunas_x: list[str] = Field(default_factory=list)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class SearchTransformPayload(SessionPayload):
|
| 62 |
+
grau_min_coef: int = 1
|
| 63 |
+
grau_min_f: int = 0
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class AdoptSuggestionPayload(SessionPayload):
|
| 67 |
+
indice: int
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class FitModelPayload(SessionPayload):
|
| 71 |
+
transformacao_y: str
|
| 72 |
+
transformacoes_x: dict[str, str] = Field(default_factory=dict)
|
| 73 |
+
dicotomicas: list[str] = Field(default_factory=list)
|
| 74 |
+
codigo_alocado: list[str] = Field(default_factory=list)
|
| 75 |
+
percentuais: list[str] = Field(default_factory=list)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class DispersaoPayload(SessionPayload):
|
| 79 |
+
tipo: str
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class OutlierFiltro(BaseModel):
|
| 83 |
+
variavel: str
|
| 84 |
+
operador: str
|
| 85 |
+
valor: float
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class OutlierFilterPayload(SessionPayload):
|
| 89 |
+
filtros: list[OutlierFiltro] = Field(default_factory=list)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class OutlierRestartPayload(SessionPayload):
|
| 93 |
+
outliers_texto: str | None = None
|
| 94 |
+
reincluir_texto: str | None = None
|
| 95 |
+
grau_min_coef: int = 3
|
| 96 |
+
grau_min_f: int = 3
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class OutlierSummaryPayload(SessionPayload):
|
| 100 |
+
outliers_texto: str | None = None
|
| 101 |
+
reincluir_texto: str | None = None
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class AvaliacaoPayload(SessionPayload):
|
| 105 |
+
valores_x: dict[str, Any]
|
| 106 |
+
indice_base: str | None = None
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class AvaliacaoDeletePayload(SessionPayload):
|
| 110 |
+
indice: str | None = None
|
| 111 |
+
indice_base: str | None = None
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class AvaliacaoBasePayload(SessionPayload):
|
| 115 |
+
indice_base: str | None = None
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class ExportModeloPayload(SessionPayload):
|
| 119 |
+
nome_arquivo: str
|
| 120 |
+
elaborador: dict[str, Any] | None = None
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class UpdateMapaPayload(SessionPayload):
|
| 124 |
+
variavel_mapa: str | None = None
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@router.post("/upload")
|
| 128 |
+
async def upload_file(
|
| 129 |
+
session_id: str = Form(...),
|
| 130 |
+
file: UploadFile = File(...),
|
| 131 |
+
) -> dict[str, Any]:
|
| 132 |
+
session = session_store.get(session_id)
|
| 133 |
+
conteudo = await file.read()
|
| 134 |
+
elaboracao_service.save_uploaded_file(session, file.filename, conteudo)
|
| 135 |
+
return elaboracao_service.load_uploaded_file(session)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@router.post("/confirm-sheet")
|
| 139 |
+
def confirm_sheet(payload: ConfirmSheetPayload) -> dict[str, Any]:
|
| 140 |
+
session = session_store.get(payload.session_id)
|
| 141 |
+
return elaboracao_service.load_uploaded_file(session, selected_sheet=payload.sheet_name)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@router.post("/map-coords")
|
| 145 |
+
def map_coords(payload: MapCoordsPayload) -> dict[str, Any]:
|
| 146 |
+
session = session_store.get(payload.session_id)
|
| 147 |
+
return elaboracao_service.mapear_coordenadas_manualmente(session, payload.col_lat, payload.col_lon)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@router.post("/geocodificar")
|
| 151 |
+
def geocodificar(payload: GeocodePayload) -> dict[str, Any]:
|
| 152 |
+
session = session_store.get(payload.session_id)
|
| 153 |
+
return elaboracao_service.geocodificar(session, payload.col_cdlog, payload.col_num, auto_200=payload.auto_200)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@router.post("/geocodificar-correcoes")
|
| 157 |
+
def geocodificar_correcoes(payload: GeocodeCorrecaoPayload) -> dict[str, Any]:
|
| 158 |
+
session = session_store.get(payload.session_id)
|
| 159 |
+
correcoes = [item.model_dump() for item in payload.correcoes]
|
| 160 |
+
return elaboracao_service.aplicar_correcoes_geocodificacao(session, correcoes, auto_200=payload.auto_200)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@router.post("/apply-selection")
|
| 164 |
+
def apply_selection(payload: ApplySelectionPayload) -> dict[str, Any]:
|
| 165 |
+
session = session_store.get(payload.session_id)
|
| 166 |
+
return elaboracao_service.apply_selection(
|
| 167 |
+
session,
|
| 168 |
+
coluna_y=payload.coluna_y,
|
| 169 |
+
colunas_x=payload.colunas_x,
|
| 170 |
+
dicotomicas=payload.dicotomicas,
|
| 171 |
+
codigo_alocado=payload.codigo_alocado,
|
| 172 |
+
percentuais=payload.percentuais,
|
| 173 |
+
outliers_anteriores=payload.outliers_anteriores,
|
| 174 |
+
grau_min_coef=payload.grau_min_coef,
|
| 175 |
+
grau_min_f=payload.grau_min_f,
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@router.post("/classify-x")
|
| 180 |
+
def classify_x(payload: ClassificarXPayload) -> dict[str, Any]:
|
| 181 |
+
session = session_store.get(payload.session_id)
|
| 182 |
+
return elaboracao_service.classificar_tipos_variaveis_x(session, payload.colunas_x)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@router.post("/search-transformations")
|
| 186 |
+
def search_transformations(payload: SearchTransformPayload) -> dict[str, Any]:
|
| 187 |
+
session = session_store.get(payload.session_id)
|
| 188 |
+
return elaboracao_service.search_transformacoes(
|
| 189 |
+
session,
|
| 190 |
+
grau_min_coef=payload.grau_min_coef,
|
| 191 |
+
grau_min_f=payload.grau_min_f,
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@router.post("/adopt-suggestion")
|
| 196 |
+
def adopt_suggestion(payload: AdoptSuggestionPayload) -> dict[str, Any]:
|
| 197 |
+
session = session_store.get(payload.session_id)
|
| 198 |
+
return elaboracao_service.adotar_sugestao(session, payload.indice)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
@router.post("/fit-model")
|
| 202 |
+
def fit_model(payload: FitModelPayload) -> dict[str, Any]:
|
| 203 |
+
session = session_store.get(payload.session_id)
|
| 204 |
+
return elaboracao_service.fit_model(
|
| 205 |
+
session,
|
| 206 |
+
transformacao_y=payload.transformacao_y,
|
| 207 |
+
transformacoes_x=payload.transformacoes_x,
|
| 208 |
+
dicotomicas=payload.dicotomicas,
|
| 209 |
+
codigo_alocado=payload.codigo_alocado,
|
| 210 |
+
percentuais=payload.percentuais,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@router.post("/model-dispersao")
|
| 215 |
+
def model_dispersao(payload: DispersaoPayload) -> dict[str, Any]:
|
| 216 |
+
session = session_store.get(payload.session_id)
|
| 217 |
+
return elaboracao_service.gerar_grafico_dispersao_modelo(session, payload.tipo)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
@router.post("/outliers/apply-filters")
|
| 221 |
+
def outliers_apply_filters(payload: OutlierFilterPayload) -> dict[str, Any]:
|
| 222 |
+
session = session_store.get(payload.session_id)
|
| 223 |
+
filtros = [item.model_dump() for item in payload.filtros]
|
| 224 |
+
return elaboracao_service.apply_outlier_filters(session, filtros)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
@router.post("/outliers/restart")
|
| 228 |
+
def outliers_restart(payload: OutlierRestartPayload) -> dict[str, Any]:
|
| 229 |
+
session = session_store.get(payload.session_id)
|
| 230 |
+
return elaboracao_service.reiniciar_iteracao(
|
| 231 |
+
session,
|
| 232 |
+
outliers_texto=payload.outliers_texto,
|
| 233 |
+
reincluir_texto=payload.reincluir_texto,
|
| 234 |
+
grau_min_coef=payload.grau_min_coef,
|
| 235 |
+
grau_min_f=payload.grau_min_f,
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@router.post("/outliers/summary")
|
| 240 |
+
def outliers_summary(payload: OutlierSummaryPayload) -> dict[str, str]:
|
| 241 |
+
session = session_store.get(payload.session_id)
|
| 242 |
+
texto = elaboracao_service.resumir_outliers(
|
| 243 |
+
session.outliers_anteriores,
|
| 244 |
+
payload.outliers_texto,
|
| 245 |
+
payload.reincluir_texto,
|
| 246 |
+
)
|
| 247 |
+
return {"resumo": texto}
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
@router.post("/outliers/clear-history")
|
| 251 |
+
def outliers_clear_history(payload: SessionPayload) -> dict[str, Any]:
|
| 252 |
+
session = session_store.get(payload.session_id)
|
| 253 |
+
return elaboracao_service.limpar_historico_outliers(session)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
@router.post("/evaluation/fields")
|
| 257 |
+
def evaluation_fields(payload: SessionPayload) -> dict[str, Any]:
|
| 258 |
+
session = session_store.get(payload.session_id)
|
| 259 |
+
return {"campos": elaboracao_service.build_campos_avaliacao(session)}
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@router.post("/evaluation/calculate")
|
| 263 |
+
def evaluation_calculate(payload: AvaliacaoPayload) -> dict[str, Any]:
|
| 264 |
+
session = session_store.get(payload.session_id)
|
| 265 |
+
return elaboracao_service.calcular_avaliacao_elaboracao(session, payload.valores_x, payload.indice_base)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@router.post("/evaluation/clear")
|
| 269 |
+
def evaluation_clear(payload: SessionPayload) -> dict[str, Any]:
|
| 270 |
+
session = session_store.get(payload.session_id)
|
| 271 |
+
return elaboracao_service.limpar_avaliacoes_elaboracao(session)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
@router.post("/evaluation/delete")
|
| 275 |
+
def evaluation_delete(payload: AvaliacaoDeletePayload) -> dict[str, Any]:
|
| 276 |
+
session = session_store.get(payload.session_id)
|
| 277 |
+
return elaboracao_service.excluir_avaliacao_elaboracao(session, payload.indice, payload.indice_base)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
@router.post("/evaluation/base")
|
| 281 |
+
def evaluation_base(payload: AvaliacaoBasePayload) -> dict[str, Any]:
|
| 282 |
+
session = session_store.get(payload.session_id)
|
| 283 |
+
return elaboracao_service.atualizar_base_avaliacao_elaboracao(session, payload.indice_base)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
@router.get("/evaluation/export")
|
| 287 |
+
def evaluation_export(session_id: str) -> FileResponse:
|
| 288 |
+
session = session_store.get(session_id)
|
| 289 |
+
caminho = elaboracao_service.exportar_avaliacoes_elaboracao(session)
|
| 290 |
+
return FileResponse(
|
| 291 |
+
path=caminho,
|
| 292 |
+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 293 |
+
filename=os.path.basename(caminho),
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
@router.get("/avaliadores")
|
| 298 |
+
def listar_avaliadores() -> dict[str, Any]:
|
| 299 |
+
return {"avaliadores": elaboracao_service.list_avaliadores()}
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
@router.post("/export-model")
|
| 303 |
+
def export_model(payload: ExportModeloPayload) -> FileResponse:
|
| 304 |
+
session = session_store.get(payload.session_id)
|
| 305 |
+
caminho, _ = elaboracao_service.exportar_modelo(session, payload.nome_arquivo, elaborador=payload.elaborador)
|
| 306 |
+
return FileResponse(
|
| 307 |
+
path=caminho,
|
| 308 |
+
media_type="application/octet-stream",
|
| 309 |
+
filename=os.path.basename(caminho),
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@router.get("/export-base")
|
| 314 |
+
def export_base(session_id: str, filtered: bool = True) -> FileResponse:
|
| 315 |
+
session = session_store.get(session_id)
|
| 316 |
+
caminho = elaboracao_service.exportar_base(session, usar_filtrado=filtered)
|
| 317 |
+
return FileResponse(
|
| 318 |
+
path=caminho,
|
| 319 |
+
media_type="text/csv",
|
| 320 |
+
filename=os.path.basename(caminho),
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
@router.post("/map/update")
|
| 325 |
+
def map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
| 326 |
+
session = session_store.get(payload.session_id)
|
| 327 |
+
return elaboracao_service.atualizar_mapa(session, payload.variavel_mapa)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
@router.get("/context")
|
| 331 |
+
def context(session_id: str) -> dict[str, Any]:
|
| 332 |
+
session = session_store.get(session_id)
|
| 333 |
+
return {
|
| 334 |
+
"coluna_y": session.coluna_y,
|
| 335 |
+
"colunas_x": session.colunas_x,
|
| 336 |
+
"dicotomicas": session.dicotomicas,
|
| 337 |
+
"codigo_alocado": session.codigo_alocado,
|
| 338 |
+
"percentuais": session.percentuais,
|
| 339 |
+
"outliers_anteriores": session.outliers_anteriores,
|
| 340 |
+
"iteracao": session.iteracao,
|
| 341 |
+
}
|
backend/app/api/health.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
router = APIRouter(tags=["health"])
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@router.get("/api/health")
|
| 10 |
+
def health() -> dict[str, str]:
|
| 11 |
+
return {"status": "ok"}
|
backend/app/api/session.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter
|
| 4 |
+
|
| 5 |
+
from app.services.session_store import session_store
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
router = APIRouter(prefix="/api/sessions", tags=["sessions"])
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@router.post("")
|
| 12 |
+
def create_session() -> dict[str, str]:
|
| 13 |
+
session = session_store.create()
|
| 14 |
+
return {"session_id": session.session_id}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@router.delete("/{session_id}")
|
| 18 |
+
def delete_session(session_id: str) -> dict[str, str]:
|
| 19 |
+
session_store.delete(session_id)
|
| 20 |
+
return {"status": "ok"}
|
backend/app/api/visualizacao.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, File, Form, UploadFile
|
| 7 |
+
from fastapi.responses import FileResponse
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
from app.services import elaboracao_service, visualizacao_service
|
| 11 |
+
from app.services.session_store import session_store
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/api/visualizacao", tags=["visualizacao"])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class SessionPayload(BaseModel):
|
| 18 |
+
session_id: str
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class MapaPayload(SessionPayload):
|
| 22 |
+
variavel_mapa: str | None = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class AvaliacaoPayload(SessionPayload):
|
| 26 |
+
valores_x: dict[str, Any]
|
| 27 |
+
indice_base: str | None = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class AvaliacaoDeletePayload(SessionPayload):
|
| 31 |
+
indice: str | None = None
|
| 32 |
+
indice_base: str | None = None
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class AvaliacaoBasePayload(SessionPayload):
|
| 36 |
+
indice_base: str | None = None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@router.post("/upload")
|
| 40 |
+
async def upload_file(
|
| 41 |
+
session_id: str = Form(...),
|
| 42 |
+
file: UploadFile = File(...),
|
| 43 |
+
) -> dict[str, Any]:
|
| 44 |
+
session = session_store.get(session_id)
|
| 45 |
+
conteudo = await file.read()
|
| 46 |
+
caminho = elaboracao_service.save_uploaded_file(session, file.filename, conteudo)
|
| 47 |
+
return visualizacao_service.carregar_modelo(session, caminho)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@router.post("/exibir")
|
| 51 |
+
def exibir(payload: SessionPayload) -> dict[str, Any]:
|
| 52 |
+
session = session_store.get(payload.session_id)
|
| 53 |
+
return visualizacao_service.exibir_modelo(session)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@router.post("/map/update")
|
| 57 |
+
def map_update(payload: MapaPayload) -> dict[str, Any]:
|
| 58 |
+
session = session_store.get(payload.session_id)
|
| 59 |
+
return visualizacao_service.atualizar_mapa(session, payload.variavel_mapa)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@router.post("/evaluation/fields")
|
| 63 |
+
def evaluation_fields(payload: SessionPayload) -> dict[str, Any]:
|
| 64 |
+
session = session_store.get(payload.session_id)
|
| 65 |
+
return {"campos": visualizacao_service.campos_avaliacao(session)}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@router.post("/evaluation/calculate")
|
| 69 |
+
def evaluation_calculate(payload: AvaliacaoPayload) -> dict[str, Any]:
|
| 70 |
+
session = session_store.get(payload.session_id)
|
| 71 |
+
return visualizacao_service.calcular_avaliacao(session, payload.valores_x, payload.indice_base)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@router.post("/evaluation/clear")
|
| 75 |
+
def evaluation_clear(payload: SessionPayload) -> dict[str, Any]:
|
| 76 |
+
session = session_store.get(payload.session_id)
|
| 77 |
+
return visualizacao_service.limpar_avaliacoes(session)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@router.post("/evaluation/delete")
|
| 81 |
+
def evaluation_delete(payload: AvaliacaoDeletePayload) -> dict[str, Any]:
|
| 82 |
+
session = session_store.get(payload.session_id)
|
| 83 |
+
return visualizacao_service.excluir_avaliacao(session, payload.indice, payload.indice_base)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@router.post("/evaluation/base")
|
| 87 |
+
def evaluation_base(payload: AvaliacaoBasePayload) -> dict[str, Any]:
|
| 88 |
+
session = session_store.get(payload.session_id)
|
| 89 |
+
return visualizacao_service.atualizar_base(session, payload.indice_base)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@router.get("/evaluation/export")
|
| 93 |
+
def evaluation_export(session_id: str) -> FileResponse:
|
| 94 |
+
session = session_store.get(session_id)
|
| 95 |
+
caminho = visualizacao_service.exportar_avaliacoes(session)
|
| 96 |
+
return FileResponse(
|
| 97 |
+
path=caminho,
|
| 98 |
+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 99 |
+
filename=os.path.basename(caminho),
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@router.post("/clear")
|
| 104 |
+
def clear(payload: SessionPayload) -> dict[str, Any]:
|
| 105 |
+
session = session_store.get(payload.session_id)
|
| 106 |
+
return visualizacao_service.limpar_tudo_visualizacao(session)
|
backend/app/core/dados/EixosLogradouros.cpg
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
UTF-8
|
backend/app/core/dados/EixosLogradouros.dbf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cf668c9b7bbcac420d23188cfb3101b7dcee2bc4b4873b8220d73afd4257f4f6
|
| 3 |
+
size 6432720
|
backend/app/core/dados/EixosLogradouros.prj
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
PROJCS["TM-POA",GEOGCS["GCS_SIRGAS_2000",DATUM["D_SIRGAS_2000",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",300000.0],PARAMETER["False_Northing",5000000.0],PARAMETER["Central_Meridian",-51.0],PARAMETER["Scale_Factor",0.999995],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
|
backend/app/core/dados/EixosLogradouros.shp
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:718687b7eff9723d63eee89749c7a338a7fe00c1cae4cb8bc952f967d5b47d23
|
| 3 |
+
size 10248444
|
backend/app/core/dados/EixosLogradouros.shp.xml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<metadata xml:lang="pt"><Esri><CreaDate>20241119</CreaDate><CreaTime>11233400</CreaTime><ArcGISFormat>1.0</ArcGISFormat><SyncOnce>FALSE</SyncOnce><DataProperties><itemProps><itemName Sync="TRUE">EixosLogradouros</itemName><imsContentType Sync="TRUE">002</imsContentType><itemSize Sync="TRUE">0.000</itemSize><itemLocation><linkage Sync="TRUE">file://\\pmpa-fs3\smams-usig$\Publicação Site PDDUA\PDDUA_shp\EixoLogradouro\EixosLogradouros.shp</linkage><protocol Sync="TRUE">Local Area Network</protocol></itemLocation></itemProps><coordRef><type Sync="TRUE">Projected</type><geogcsn Sync="TRUE">GCS_SIRGAS_2000</geogcsn><csUnits Sync="TRUE">Linear Unit: Meter (1.000000)</csUnits><projcsn Sync="TRUE">TM-POA</projcsn><peXml Sync="TRUE"><ProjectedCoordinateSystem xsi:type='typens:ProjectedCoordinateSystem' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:typens='http://www.esri.com/schemas/ArcGIS/10.7'><WKT>PROJCS[&quot;TM-POA&quot;,GEOGCS[&quot;GCS_SIRGAS_2000&quot;,DATUM[&quot;D_SIRGAS_2000&quot;,SPHEROID[&quot;GRS_1980&quot;,6378137.0,298.257222101]],PRIMEM[&quot;Greenwich&quot;,0.0],UNIT[&quot;Degree&quot;,0.0174532925199433]],PROJECTION[&quot;Transverse_Mercator&quot;],PARAMETER[&quot;False_Easting&quot;,300000.0],PARAMETER[&quot;False_Northing&quot;,5000000.0],PARAMETER[&quot;Central_Meridian&quot;,-51.0],PARAMETER[&quot;Scale_Factor&quot;,0.999995],PARAMETER[&quot;Latitude_Of_Origin&quot;,0.0],UNIT[&quot;Meter&quot;,1.0]]</WKT><XOrigin>-5323100</XOrigin><YOrigin>-5002100</YOrigin><XYScale>450265407.00157917</XYScale><ZOrigin>-100000</ZOrigin><ZScale>10000</ZScale><MOrigin>-100000</MOrigin><MScale>10000</MScale><XYTolerance>0.001</XYTolerance><ZTolerance>0.001</ZTolerance><MTolerance>0.001</MTolerance><HighPrecision>true</HighPrecision></ProjectedCoordinateSystem></peXml></coordRef><lineage><Process ToolSource="c:\program files (x86)\arcgis\desktop10.7\ArcToolbox\Toolboxes\Conversion Tools.tbx\FeatureClassToFeatureClass" Date="20241119" Time="112349">FeatureClassToFeatureClass "Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV" "Y:\Publicação Site PDDUA\PDDUA_shp\EixoLogradouro" EixosLogradouros.shp # "CDLOG "CDLOG" true true false 4 Long 0 7 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,CDLOG,-1,-1;CDSEG "CDSEG" true true false 4 Long 0 6 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,CDSEG,-1,-1;NRIMPINI "NRIMPINI" true true false 4 Long 0 5 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,NRIMPINI,-1,-1;NRIMPFIN "NRIMPFIN" true true false 4 Long 0 5 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,NRIMPFIN,-1,-1;NRPARINI "NRPARINI" true true false 4 Long 0 5 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,NRPARINI,-1,-1;NRPARFIN "NRPARFIN" true true false 4 Long 0 5 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,NRPARFIN,-1,-1;MTORIGEM "MTORIGEM" true true false 20 Text 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,MTORIGEM,-1,-1;MTQUAL "MTQUAL" true true false 10 Text 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,MTQUAL,-1,-1;CDTIPO "CDTIPO" true true false 2 Short 0 2 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,CDTIPO,-1,-1;MTVETOR "MTVETOR" true true false 26 Text 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,MTVETOR,-1,-1;FGCOMP "FGCOMP" true true false 8 Double 2 13 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,FGCOMP,-1,-1;SEL "SEL" true true false 2 Short 0 2 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,SEL,-1,-1;LABEL "LABEL" true true false 2 Short 0 4 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,LABEL,-1,-1;CDIDELOG "CDIDELOG" true false false 4 Long 0 7 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,CDIDELOG,-1,-1;NMIDELOG "NMIDELOG" true false false 37 Text 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,NMIDELOG,-1,-1;CDIDECAT "CDIDECAT" true false false 5 Text 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,CDIDECAT,-1,-1;NMIDEPRE "NMIDEPRE" true false false 3 Text 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,NMIDEPRE,-1,-1;NMIDEABR "NMIDEABR" true false false 16 Text 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,NMIDEABR,-1,-1;GEOM_LEN "GEOM_LEN" false false true 0 Double 0 0 ,First,#,Database Connections\GEOEIXOS.sde\GEOSMURB.EIXOS_CONV_VIEW\GEOSMURB.EIXOS_VIEW_CONV,GEOM.LEN,-1,-1" #</Process></lineage></DataProperties><SyncDate>20241119</SyncDate><SyncTime>11233400</SyncTime><ModDate>20241119</ModDate><ModTime>11233400</ModTime></Esri><dataIdInfo><envirDesc Sync="TRUE"> Version 6.2 (Build 9200) ; Esri ArcGIS 10.7.0.10348</envirDesc><dataLang><languageCode value="por" Sync="TRUE"></languageCode><countryCode value="BRA" Sync="TRUE"></countryCode></dataLang><idCitation><resTitle Sync="TRUE">EixosLogradouros</resTitle><presForm><PresFormCd value="005" Sync="TRUE"></PresFormCd></presForm></idCitation><spatRpType><SpatRepTypCd value="001" Sync="TRUE"></SpatRepTypCd></spatRpType></dataIdInfo><mdLang><languageCode value="por" Sync="TRUE"></languageCode><countryCode value="BRA" Sync="TRUE"></countryCode></mdLang><mdChar><CharSetCd value="004" Sync="TRUE"></CharSetCd></mdChar><distInfo><distFormat><formatName Sync="TRUE">Shapefile</formatName></distFormat><distTranOps><transSize Sync="TRUE">0.000</transSize></distTranOps></distInfo><mdHrLv><ScopeCd value="005" Sync="TRUE"></ScopeCd></mdHrLv><mdHrLvName Sync="TRUE">dataset</mdHrLvName><refSysInfo><RefSystem><refSysID><identCode code="0" Sync="TRUE"></identCode></refSysID></RefSystem></refSysInfo><spatRepInfo><VectSpatRep><geometObjs Name="EixosLogradouros"><geoObjTyp><GeoObjTypCd value="002" Sync="TRUE"></GeoObjTypCd></geoObjTyp><geoObjCnt Sync="TRUE">0</geoObjCnt></geometObjs><topLvl><TopoLevCd value="001" Sync="TRUE"></TopoLevCd></topLvl></VectSpatRep></spatRepInfo><spdoinfo><ptvctinf><esriterm Name="EixosLogradouros"><efeatyp Sync="TRUE">Simple</efeatyp><efeageom code="3" Sync="TRUE"></efeageom><esritopo Sync="TRUE">FALSE</esritopo><efeacnt Sync="TRUE">0</efeacnt><spindex Sync="TRUE">FALSE</spindex><linrefer Sync="TRUE">FALSE</linrefer></esriterm></ptvctinf></spdoinfo><eainfo><detailed Name="EixosLogradouros"><enttyp><enttypl Sync="TRUE">EixosLogradouros</enttypl><enttypt Sync="TRUE">Feature Class</enttypt><enttypc Sync="TRUE">0</enttypc></enttyp><attr><attrlabl Sync="TRUE">FID</attrlabl><attalias Sync="TRUE">FID</attalias><attrtype Sync="TRUE">OID</attrtype><attwidth Sync="TRUE">4</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale><attrdef Sync="TRUE">Internal feature number.</attrdef><attrdefs Sync="TRUE">Esri</attrdefs><attrdomv><udom Sync="TRUE">Sequential unique whole numbers that are automatically generated.</udom></attrdomv></attr><attr><attrlabl Sync="TRUE">Shape</attrlabl><attalias Sync="TRUE">Shape</attalias><attrtype Sync="TRUE">Geometry</attrtype><attwidth Sync="TRUE">0</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale><attrdef Sync="TRUE">Feature geometry.</attrdef><attrdefs Sync="TRUE">Esri</attrdefs><attrdomv><udom Sync="TRUE">Coordinates defining the features.</udom></attrdomv></attr><attr><attrlabl Sync="TRUE">CDLOG</attrlabl><attalias Sync="TRUE">CDLOG</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">7</attwidth><atprecis Sync="TRUE">7</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">CDSEG</attrlabl><attalias Sync="TRUE">CDSEG</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">6</attwidth><atprecis Sync="TRUE">6</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NRIMPINI</attrlabl><attalias Sync="TRUE">NRIMPINI</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">5</attwidth><atprecis Sync="TRUE">5</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NRIMPFIN</attrlabl><attalias Sync="TRUE">NRIMPFIN</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">5</attwidth><atprecis Sync="TRUE">5</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NRPARINI</attrlabl><attalias Sync="TRUE">NRPARINI</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">5</attwidth><atprecis Sync="TRUE">5</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NRPARFIN</attrlabl><attalias Sync="TRUE">NRPARFIN</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">5</attwidth><atprecis Sync="TRUE">5</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">MTORIGEM</attrlabl><attalias Sync="TRUE">MTORIGEM</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">20</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">MTQUAL</attrlabl><attalias Sync="TRUE">MTQUAL</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">10</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">CDTIPO</attrlabl><attalias Sync="TRUE">CDTIPO</attalias><attrtype Sync="TRUE">SmallInteger</attrtype><attwidth Sync="TRUE">2</attwidth><atprecis Sync="TRUE">2</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">MTVETOR</attrlabl><attalias Sync="TRUE">MTVETOR</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">26</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">FGCOMP</attrlabl><attalias Sync="TRUE">FGCOMP</attalias><attrtype Sync="TRUE">Double</attrtype><attwidth Sync="TRUE">14</attwidth><atprecis Sync="TRUE">13</atprecis><attscale Sync="TRUE">2</attscale></attr><attr><attrlabl Sync="TRUE">SEL</attrlabl><attalias Sync="TRUE">SEL</attalias><attrtype Sync="TRUE">SmallInteger</attrtype><attwidth Sync="TRUE">2</attwidth><atprecis Sync="TRUE">2</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">LABEL</attrlabl><attalias Sync="TRUE">LABEL</attalias><attrtype Sync="TRUE">SmallInteger</attrtype><attwidth Sync="TRUE">4</attwidth><atprecis Sync="TRUE">4</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">CDIDELOG</attrlabl><attalias Sync="TRUE">CDIDELOG</attalias><attrtype Sync="TRUE">Integer</attrtype><attwidth Sync="TRUE">7</attwidth><atprecis Sync="TRUE">7</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NMIDELOG</attrlabl><attalias Sync="TRUE">NMIDELOG</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">37</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">CDIDECAT</attrlabl><attalias Sync="TRUE">CDIDECAT</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">5</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NMIDEPRE</attrlabl><attalias Sync="TRUE">NMIDEPRE</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">3</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">NMIDEABR</attrlabl><attalias Sync="TRUE">NMIDEABR</attalias><attrtype Sync="TRUE">String</attrtype><attwidth Sync="TRUE">16</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr><attr><attrlabl Sync="TRUE">GEOM_LEN</attrlabl><attalias Sync="TRUE">GEOM_LEN</attalias><attrtype Sync="TRUE">Double</attrtype><attwidth Sync="TRUE">19</attwidth><atprecis Sync="TRUE">0</atprecis><attscale Sync="TRUE">0</attscale></attr></detailed></eainfo><mdDateSt Sync="TRUE">20241119</mdDateSt></metadata>
|
backend/app/core/dados/EixosLogradouros.shx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee0d38b37aba5823e51880dbcf6244c881397fa0e92b765806a28472388f36c1
|
| 3 |
+
size 258676
|
backend/app/core/elaboracao/__init__.py
ADDED
|
File without changes
|
backend/app/core/elaboracao/app.py
ADDED
|
@@ -0,0 +1,1910 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
app.py - Interface Gradio para elaboração de modelos estatísticos.
|
| 4 |
+
|
| 5 |
+
Contém criar_aba() (construção de UI + event wiring) e callbacks auxiliares pequenos.
|
| 6 |
+
Lógica de negócio delegada a módulos: modelo.py, carregamento.py, outliers.py, formatadores.py.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import os
|
| 12 |
+
import json
|
| 13 |
+
|
| 14 |
+
_avaliadores_path = os.path.join(os.path.dirname(__file__), "avaliadores.json")
|
| 15 |
+
with open(_avaliadores_path, encoding="utf-8") as _f:
|
| 16 |
+
_avaliadores_raw = json.load(_f)
|
| 17 |
+
_avaliadores_lista = _avaliadores_raw.get("avaliadores", [])
|
| 18 |
+
_avaliadores_nomes = [a["nome_completo"] for a in _avaliadores_lista]
|
| 19 |
+
_avaliadores_dict = {a["nome_completo"]: a for a in _avaliadores_lista}
|
| 20 |
+
|
| 21 |
+
# Módulos locais — lógica de negócio
|
| 22 |
+
from .core import (
|
| 23 |
+
obter_colunas_numericas,
|
| 24 |
+
exportar_base_csv,
|
| 25 |
+
TRANSFORMACOES,
|
| 26 |
+
)
|
| 27 |
+
from .charts import criar_mapa
|
| 28 |
+
|
| 29 |
+
# Módulos locais — formatação
|
| 30 |
+
from .formatadores import TITULO, carregar_css, criar_header_secao
|
| 31 |
+
|
| 32 |
+
# Módulos locais — callbacks de domínio
|
| 33 |
+
from .modelo import (
|
| 34 |
+
MAX_VARS_X,
|
| 35 |
+
ao_mudar_tipo_grafico,
|
| 36 |
+
ao_mudar_y_sem_estatisticas,
|
| 37 |
+
aplicar_selecao_callback,
|
| 38 |
+
buscar_transformacoes_callback,
|
| 39 |
+
ajustar_modelo_callback,
|
| 40 |
+
_atualizar_campos_transformacoes_com_flag,
|
| 41 |
+
adotar_sugestao,
|
| 42 |
+
exportar_modelo_callback,
|
| 43 |
+
popular_campos_avaliacao_callback,
|
| 44 |
+
avaliar_imovel_callback,
|
| 45 |
+
limpar_avaliacoes_callback,
|
| 46 |
+
excluir_avaliacao_callback,
|
| 47 |
+
atualizar_base_avaliacao_callback,
|
| 48 |
+
exportar_avaliacoes_excel_callback,
|
| 49 |
+
atualizar_interativo_dicotomicas,
|
| 50 |
+
popular_dicotomicas_callback,
|
| 51 |
+
)
|
| 52 |
+
from .carregamento import (
|
| 53 |
+
ao_carregar_arquivo,
|
| 54 |
+
confirmar_aba_callback,
|
| 55 |
+
limpar_historico_callback,
|
| 56 |
+
)
|
| 57 |
+
from .geocodificacao import (
|
| 58 |
+
padronizar_coords,
|
| 59 |
+
geocodificar,
|
| 60 |
+
aplicar_correcoes_e_regeodificar,
|
| 61 |
+
formatar_status_geocodificacao,
|
| 62 |
+
preparar_display_falhas,
|
| 63 |
+
)
|
| 64 |
+
from .outliers import (
|
| 65 |
+
aplicar_filtros_callback,
|
| 66 |
+
adicionar_filtro_callback,
|
| 67 |
+
remover_ultimo_filtro_callback,
|
| 68 |
+
limpar_filtros_callback,
|
| 69 |
+
reiniciar_iteracao_callback,
|
| 70 |
+
atualizar_resumo_outliers,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ============================================================
|
| 75 |
+
# CALLBACKS AUXILIARES (pequenos, diretamente ligados ao event wiring)
|
| 76 |
+
# ============================================================
|
| 77 |
+
|
| 78 |
+
def download_tabela_callback(df, prefixo="tabela"):
|
| 79 |
+
"""Callback genérico para download de DataFrame como CSV."""
|
| 80 |
+
if df is None:
|
| 81 |
+
return gr.update(value=None, visible=False)
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
import tempfile
|
| 85 |
+
|
| 86 |
+
if isinstance(df, pd.DataFrame):
|
| 87 |
+
if df.empty:
|
| 88 |
+
return gr.update(value=None, visible=False)
|
| 89 |
+
|
| 90 |
+
fd, caminho = tempfile.mkstemp(suffix=".csv", prefix=f"{prefixo}_")
|
| 91 |
+
os.close(fd)
|
| 92 |
+
df.to_csv(caminho, index=True, encoding="utf-8-sig", sep=";", decimal=",")
|
| 93 |
+
return gr.update(value=caminho, visible=True)
|
| 94 |
+
else:
|
| 95 |
+
return gr.update(value=None, visible=False)
|
| 96 |
+
except Exception as e:
|
| 97 |
+
print(f"Erro ao exportar tabela: {e}")
|
| 98 |
+
return gr.update(value=None, visible=False)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _extrair_var_mapa(var_mapa):
|
| 102 |
+
"""Retorna None se for 'Visualização Padrão' ou vazio, senão retorna o nome da variável."""
|
| 103 |
+
if not var_mapa or var_mapa == "Visualização Padrão":
|
| 104 |
+
return None
|
| 105 |
+
return var_mapa
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def ao_clicar_tabela(df, var_mapa, evt: gr.SelectData):
|
| 109 |
+
"""Callback quando clica em linha da tabela."""
|
| 110 |
+
if df is None or evt is None:
|
| 111 |
+
return "<p>Carregue dados para ver o mapa.</p>"
|
| 112 |
+
|
| 113 |
+
# Pega o índice da linha clicada
|
| 114 |
+
indice = evt.index[0] + 1 # Ajusta para índice baseado em 1
|
| 115 |
+
|
| 116 |
+
return criar_mapa(df, indice_destacado=indice, tamanho_col=_extrair_var_mapa(var_mapa))
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def atualizar_mapa_callback(df_filtrado, df_original, var_mapa):
|
| 120 |
+
"""Callback quando a variável de dimensionamento do mapa é alterada."""
|
| 121 |
+
df = df_filtrado if df_filtrado is not None else df_original
|
| 122 |
+
if df is None:
|
| 123 |
+
return "<p>Carregue dados para ver o mapa.</p>"
|
| 124 |
+
return criar_mapa(df, tamanho_col=_extrair_var_mapa(var_mapa))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
MAX_FALHAS_GEO = 20 # Slots pré-alocados para correção individual de falhas de geocodificação
|
| 128 |
+
|
| 129 |
+
# ============================================================
|
| 130 |
+
# CALLBACKS DE RESOLUÇÃO DE COORDENADAS (Seção 1 — painel lat/lon)
|
| 131 |
+
# ============================================================
|
| 132 |
+
|
| 133 |
+
def _revelar_secao_2(df, mapa):
|
| 134 |
+
"""Retorna os 9 valores comuns a todos os callbacks de resolução de coords."""
|
| 135 |
+
from datetime import datetime, timezone, timedelta
|
| 136 |
+
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 137 |
+
ts = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 138 |
+
return (
|
| 139 |
+
df, # estado_df
|
| 140 |
+
df, # estado_df_filtrado
|
| 141 |
+
mapa, # mapa_html
|
| 142 |
+
gr.update(visible=False), # row_coords_panel
|
| 143 |
+
gr.update(visible=True, value=criar_header_secao(2, "Visualizar Dados", ts)), # header_secao_2
|
| 144 |
+
gr.update(visible=True, open=True), # accordion_secao_2
|
| 145 |
+
gr.update(visible=True), # header_secao_3
|
| 146 |
+
gr.update(visible=True, open=True), # accordion_secao_3
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def confirmar_mapeamento_callback(df, col_lat, col_lon):
|
| 151 |
+
"""Opção 1: copia col_lat → 'lat' e col_lon → 'lon', libera Seção 2.
|
| 152 |
+
|
| 153 |
+
CONTRACT: 10 itens — html_erro_mapeamento + 8 de _revelar_secao_2 + status
|
| 154 |
+
|
| 155 |
+
Valida 4 aspectos antes de aceitar as colunas:
|
| 156 |
+
1. Conversibilidade numérica (≥50% dos valores)
|
| 157 |
+
2. Limites teóricos (lat −90/+90, lon −180/+180, ≥80%)
|
| 158 |
+
3. Inversão entre colunas (lat parece lon e vice-versa)
|
| 159 |
+
4. Casas decimais (coordenadas geográficas raramente são inteiros exatos)
|
| 160 |
+
"""
|
| 161 |
+
def _erro_mapeamento(linhas_html):
|
| 162 |
+
html = (
|
| 163 |
+
'<div style="color:#c0392b;margin-top:6px;padding:8px 12px;'
|
| 164 |
+
'background:#fdf2f2;border-radius:6px;border-left:3px solid #c0392b">'
|
| 165 |
+
'<strong>⚠ Diagnóstico das colunas selecionadas:</strong><br>'
|
| 166 |
+
+ linhas_html +
|
| 167 |
+
'</div>'
|
| 168 |
+
)
|
| 169 |
+
return (
|
| 170 |
+
gr.update(value=html),
|
| 171 |
+
gr.update(), gr.update(), gr.update(),
|
| 172 |
+
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
| 173 |
+
gr.update(),
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
if df is None or not col_lat or not col_lon:
|
| 177 |
+
return _erro_mapeamento("Selecione as colunas de latitude e longitude antes de confirmar.")
|
| 178 |
+
|
| 179 |
+
n_total = len(df)
|
| 180 |
+
if n_total == 0:
|
| 181 |
+
return _erro_mapeamento("O DataFrame está vazio.")
|
| 182 |
+
|
| 183 |
+
# Aspecto 1 — Conversibilidade numérica
|
| 184 |
+
lat_num = pd.to_numeric(df[col_lat], errors="coerce")
|
| 185 |
+
lon_num = pd.to_numeric(df[col_lon], errors="coerce")
|
| 186 |
+
lat_vals = lat_num.dropna()
|
| 187 |
+
lon_vals = lon_num.dropna()
|
| 188 |
+
lat_conv = len(lat_vals) / n_total
|
| 189 |
+
lon_conv = len(lon_vals) / n_total
|
| 190 |
+
|
| 191 |
+
erros_conv = []
|
| 192 |
+
if lat_conv < 0.5:
|
| 193 |
+
erros_conv.append(
|
| 194 |
+
f"• <strong>{col_lat}</strong>: apenas {lat_conv:.0%} dos valores são numéricos "
|
| 195 |
+
f"— a coluna provavelmente não contém coordenadas."
|
| 196 |
+
)
|
| 197 |
+
if lon_conv < 0.5:
|
| 198 |
+
erros_conv.append(
|
| 199 |
+
f"• <strong>{col_lon}</strong>: apenas {lon_conv:.0%} dos valores são numéricos "
|
| 200 |
+
f"— a coluna provavelmente não contém coordenadas."
|
| 201 |
+
)
|
| 202 |
+
if erros_conv:
|
| 203 |
+
return _erro_mapeamento("<br>".join(erros_conv))
|
| 204 |
+
|
| 205 |
+
# Aspecto 2 — Limites teóricos
|
| 206 |
+
lat_in_range = ((lat_vals >= -90) & (lat_vals <= 90)).mean()
|
| 207 |
+
lon_in_range = ((lon_vals >= -180) & (lon_vals <= 180)).mean()
|
| 208 |
+
|
| 209 |
+
# Aspecto 3 — Inversão de colunas
|
| 210 |
+
lat_in_lon_range = ((lat_vals >= -180) & (lat_vals <= 180)).mean()
|
| 211 |
+
lon_in_lat_range = ((lon_vals >= -90) & (lon_vals <= 90)).mean()
|
| 212 |
+
inversao = lat_in_range < 0.5 and lat_in_lon_range > 0.8 and lon_in_lat_range > 0.8
|
| 213 |
+
|
| 214 |
+
# Aspecto 4 — Casas decimais
|
| 215 |
+
lat_tem_decimais = (lat_vals % 1 != 0).mean() if len(lat_vals) > 0 else 0.0
|
| 216 |
+
lon_tem_decimais = (lon_vals % 1 != 0).mean() if len(lon_vals) > 0 else 0.0
|
| 217 |
+
|
| 218 |
+
linhas = []
|
| 219 |
+
|
| 220 |
+
if inversao:
|
| 221 |
+
linhas.append(
|
| 222 |
+
f"• As colunas parecem estar <strong>invertidas</strong>: "
|
| 223 |
+
f"<strong>{col_lat}</strong> tem {1 - lat_in_range:.0%} dos valores fora de −90 a +90 "
|
| 224 |
+
f"(fora do intervalo de latitude), mas dentro de −180 a +180 (intervalo de longitude). "
|
| 225 |
+
f"<strong>{col_lon}</strong> tem {lon_in_lat_range:.0%} dos valores em −90 a +90 "
|
| 226 |
+
f"(faixa típica de latitude). Tente trocar as colunas."
|
| 227 |
+
)
|
| 228 |
+
else:
|
| 229 |
+
if lat_in_range < 0.8:
|
| 230 |
+
linhas.append(
|
| 231 |
+
f"• <strong>{col_lat}</strong>: apenas {lat_in_range:.0%} dos valores estão "
|
| 232 |
+
f"em −90 a +90 (esperado para latitude)."
|
| 233 |
+
)
|
| 234 |
+
if lon_in_range < 0.8:
|
| 235 |
+
linhas.append(
|
| 236 |
+
f"• <strong>{col_lon}</strong>: apenas {lon_in_range:.0%} dos valores estão "
|
| 237 |
+
f"em −180 a +180 (esperado para longitude)."
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# Aspecto 4 só é avaliado se não há outros problemas (seria ruído adicional)
|
| 241 |
+
if not linhas:
|
| 242 |
+
if lat_tem_decimais < 0.1:
|
| 243 |
+
linhas.append(
|
| 244 |
+
f"• <strong>{col_lat}</strong>: {lat_tem_decimais:.0%} dos valores têm casas decimais "
|
| 245 |
+
f"— coordenadas geográficas normalmente são valores de ponto flutuante, não inteiros exatos."
|
| 246 |
+
)
|
| 247 |
+
if lon_tem_decimais < 0.1:
|
| 248 |
+
linhas.append(
|
| 249 |
+
f"• <strong>{col_lon}</strong>: {lon_tem_decimais:.0%} dos valores têm casas decimais "
|
| 250 |
+
f"— coordenadas geográficas normalmente são valores de ponto flutuante, não inteiros exatos."
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
if linhas:
|
| 254 |
+
return _erro_mapeamento("<br>".join(linhas))
|
| 255 |
+
|
| 256 |
+
df_novo = padronizar_coords(df, col_lat, col_lon)
|
| 257 |
+
mapa = criar_mapa(df_novo)
|
| 258 |
+
return (
|
| 259 |
+
gr.update(value=""), # html_erro_mapeamento — limpar
|
| 260 |
+
) + _revelar_secao_2(df_novo, mapa) + ("Coordenadas mapeadas. Seção 2 liberada.",)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def geocodificar_callback(df, col_cdlog, col_num, auto_200):
|
| 264 |
+
"""Opção 3: executa geocodificação por interpolação em eixos.
|
| 265 |
+
|
| 266 |
+
CONTRACT: 65 itens —
|
| 267 |
+
estado_geo_temp, estado_df_falhas, html_geo_status,
|
| 268 |
+
20×falha_rows, 20×falha_htmls, 20×falha_inputs,
|
| 269 |
+
btn_aplicar_correcoes_geo, btn_usar_coords_geo
|
| 270 |
+
"""
|
| 271 |
+
N = MAX_FALHAS_GEO
|
| 272 |
+
_no_slots = (
|
| 273 |
+
*[gr.update(visible=False)] * N,
|
| 274 |
+
*[gr.update(value="")] * N,
|
| 275 |
+
*[gr.update(value=None)] * N,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
if df is None or not col_cdlog or not col_num:
|
| 279 |
+
return (
|
| 280 |
+
None, None,
|
| 281 |
+
"<p>Selecione as colunas CDLOG e Número Predial.</p>",
|
| 282 |
+
*_no_slots,
|
| 283 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 284 |
+
)
|
| 285 |
+
try:
|
| 286 |
+
df_resultado, df_falhas, ajustados = geocodificar(df, col_cdlog, col_num, auto_200)
|
| 287 |
+
except RuntimeError as e:
|
| 288 |
+
return (
|
| 289 |
+
None, None,
|
| 290 |
+
f'<div style="color:red;padding:8px">{e}</div>',
|
| 291 |
+
*_no_slots,
|
| 292 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
html_status = formatar_status_geocodificacao(df_resultado, df_falhas, ajustados)
|
| 296 |
+
n_falhas = len(df_falhas)
|
| 297 |
+
tem_coords = df_resultado["lat"].notna().any() if "lat" in df_resultado.columns else False
|
| 298 |
+
completo = n_falhas == 0 and tem_coords
|
| 299 |
+
|
| 300 |
+
if n_falhas > N:
|
| 301 |
+
html_status += (
|
| 302 |
+
f'<div style="color:#c0392b;margin-top:6px">⚠ {n_falhas} falhas excedem o limite '
|
| 303 |
+
f'de {N} correções manuais. Corrija os endereços na planilha-fonte e recarregue.</div>'
|
| 304 |
+
)
|
| 305 |
+
return (
|
| 306 |
+
df_resultado, None,
|
| 307 |
+
html_status,
|
| 308 |
+
*_no_slots,
|
| 309 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
# Monta atualizações para os N slots
|
| 313 |
+
row_updates, html_updates, num_updates = [], [], []
|
| 314 |
+
for i in range(N):
|
| 315 |
+
if i < n_falhas:
|
| 316 |
+
row = df_falhas.iloc[i]
|
| 317 |
+
sugestoes = row.get("sugestoes", "")
|
| 318 |
+
linha = row["_idx"]
|
| 319 |
+
row_updates.append(gr.update(visible=True))
|
| 320 |
+
html_updates.append(gr.update(value=(
|
| 321 |
+
f'<div style="padding:4px 8px;background:#fff8f0;border-left:3px solid #f39c12;'
|
| 322 |
+
f'border-radius:4px;font-size:0.9em">'
|
| 323 |
+
f'<strong>Linha {linha}</strong> · CDLOG {row["cdlog"]} · '
|
| 324 |
+
f'Nº atual: <strong>{row["numero_atual"]}</strong> · {row["motivo"]}'
|
| 325 |
+
+ (f'<br><span style="color:#555">Sugestões: {sugestoes}</span>' if sugestoes else '')
|
| 326 |
+
+ '</div>'
|
| 327 |
+
)))
|
| 328 |
+
num_updates.append(gr.update(value=None, label=f"Nº Corrigido (linha {linha})"))
|
| 329 |
+
else:
|
| 330 |
+
row_updates.append(gr.update(visible=False))
|
| 331 |
+
html_updates.append(gr.update(value=""))
|
| 332 |
+
num_updates.append(gr.update(value=None))
|
| 333 |
+
|
| 334 |
+
return (
|
| 335 |
+
df_resultado, # estado_geo_temp
|
| 336 |
+
df_falhas if n_falhas > 0 else None, # estado_df_falhas
|
| 337 |
+
html_status, # html_geo_status
|
| 338 |
+
*row_updates, # 20 falha_rows
|
| 339 |
+
*html_updates, # 20 falha_htmls
|
| 340 |
+
*num_updates, # 20 falha_inputs
|
| 341 |
+
gr.update(visible=n_falhas > 0), # btn_aplicar_correcoes_geo
|
| 342 |
+
gr.update(visible=completo), # btn_usar_coords_geo
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def aplicar_correcoes_geo_callback(df_original, df_falhas, *args):
|
| 347 |
+
"""Opção 3: aplica correções manuais e re-geocodifica.
|
| 348 |
+
|
| 349 |
+
CONTRACT: inputs = estado_geo_temp (1) + estado_df_falhas (1) + 20 falha_inputs + 3 params = 25.
|
| 350 |
+
CONTRACT: 65 itens retornados (mesmo formato de geocodificar_callback).
|
| 351 |
+
"""
|
| 352 |
+
N = MAX_FALHAS_GEO
|
| 353 |
+
correcoes_vals = list(args[:N])
|
| 354 |
+
col_cdlog, col_num, auto_200 = args[N], args[N + 1], args[N + 2]
|
| 355 |
+
|
| 356 |
+
_no_slots = (
|
| 357 |
+
*[gr.update(visible=False)] * N,
|
| 358 |
+
*[gr.update(value="")] * N,
|
| 359 |
+
*[gr.update(value=None)] * N,
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
if df_original is None or df_falhas is None or not col_cdlog or not col_num:
|
| 363 |
+
return (
|
| 364 |
+
None, None,
|
| 365 |
+
"<p>Sem dados para processar.</p>",
|
| 366 |
+
*_no_slots,
|
| 367 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
# Monta coluna "numero_corrigido" a partir dos valores dos gr.Number
|
| 371 |
+
df_falhas_fmt = df_falhas.copy()
|
| 372 |
+
num_corrigidos = []
|
| 373 |
+
for i, val in enumerate(correcoes_vals[:len(df_falhas_fmt)]):
|
| 374 |
+
if val is not None and not (isinstance(val, float) and pd.isna(val)):
|
| 375 |
+
num_corrigidos.append(str(int(val)))
|
| 376 |
+
else:
|
| 377 |
+
num_corrigidos.append("")
|
| 378 |
+
# Preenche com "" os slots além do tamanho real de df_falhas
|
| 379 |
+
num_corrigidos += [""] * max(0, len(df_falhas_fmt) - len(num_corrigidos))
|
| 380 |
+
df_falhas_fmt["numero_corrigido"] = num_corrigidos
|
| 381 |
+
|
| 382 |
+
try:
|
| 383 |
+
df_resultado, df_falhas_novas, ajustados, manuais = aplicar_correcoes_e_regeodificar(
|
| 384 |
+
df_original, df_falhas_fmt, col_cdlog, col_num, auto_200
|
| 385 |
+
)
|
| 386 |
+
except RuntimeError as e:
|
| 387 |
+
return (
|
| 388 |
+
None, None,
|
| 389 |
+
f'<div style="color:red;padding:8px">{e}</div>',
|
| 390 |
+
*_no_slots,
|
| 391 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
html_status = formatar_status_geocodificacao(df_resultado, df_falhas_novas, ajustados, manuais)
|
| 395 |
+
n_falhas = len(df_falhas_novas)
|
| 396 |
+
tem_coords = df_resultado["lat"].notna().any() if "lat" in df_resultado.columns else False
|
| 397 |
+
completo = n_falhas == 0 and tem_coords
|
| 398 |
+
|
| 399 |
+
if n_falhas > N:
|
| 400 |
+
html_status += (
|
| 401 |
+
f'<div style="color:#c0392b;margin-top:6px">⚠ {n_falhas} falhas excedem o limite '
|
| 402 |
+
f'de {N} correções manuais. Corrija os endereços na planilha-fonte e recarregue.</div>'
|
| 403 |
+
)
|
| 404 |
+
return (
|
| 405 |
+
df_resultado, None,
|
| 406 |
+
html_status,
|
| 407 |
+
*_no_slots,
|
| 408 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
row_updates, html_updates, num_updates = [], [], []
|
| 412 |
+
for i in range(N):
|
| 413 |
+
if i < n_falhas:
|
| 414 |
+
row = df_falhas_novas.iloc[i]
|
| 415 |
+
sugestoes = row.get("sugestoes", "")
|
| 416 |
+
linha = row["_idx"]
|
| 417 |
+
row_updates.append(gr.update(visible=True))
|
| 418 |
+
html_updates.append(gr.update(value=(
|
| 419 |
+
f'<div style="padding:4px 8px;background:#fff8f0;border-left:3px solid #f39c12;'
|
| 420 |
+
f'border-radius:4px;font-size:0.9em">'
|
| 421 |
+
f'<strong>Linha {linha}</strong> · CDLOG {row["cdlog"]} · '
|
| 422 |
+
f'Nº atual: <strong>{row["numero_atual"]}</strong> · {row["motivo"]}'
|
| 423 |
+
+ (f'<br><span style="color:#555">Sugestões: {sugestoes}</span>' if sugestoes else '')
|
| 424 |
+
+ '</div>'
|
| 425 |
+
)))
|
| 426 |
+
num_updates.append(gr.update(value=None, label=f"Nº Corrigido (linha {linha})"))
|
| 427 |
+
else:
|
| 428 |
+
row_updates.append(gr.update(visible=False))
|
| 429 |
+
html_updates.append(gr.update(value=""))
|
| 430 |
+
num_updates.append(gr.update(value=None))
|
| 431 |
+
|
| 432 |
+
return (
|
| 433 |
+
df_resultado,
|
| 434 |
+
df_falhas_novas if n_falhas > 0 else None,
|
| 435 |
+
html_status,
|
| 436 |
+
*row_updates,
|
| 437 |
+
*html_updates,
|
| 438 |
+
*num_updates,
|
| 439 |
+
gr.update(visible=n_falhas > 0), # btn_aplicar_correcoes_geo
|
| 440 |
+
gr.update(visible=completo), # btn_usar_coords_geo
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def confirmar_geocodificacao_callback(df_geo):
|
| 445 |
+
"""Opção 3: confirma uso das coords geocodificadas e libera Seção 2."""
|
| 446 |
+
if df_geo is None:
|
| 447 |
+
return (
|
| 448 |
+
gr.update(), gr.update(), gr.update(),
|
| 449 |
+
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
| 450 |
+
"Sem dados geocodificados.",
|
| 451 |
+
)
|
| 452 |
+
mapa = criar_mapa(df_geo)
|
| 453 |
+
return _revelar_secao_2(df_geo, mapa) + ("Geocodificação concluída. Seção 2 liberada.",)
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def confirmar_sem_coords_callback(df):
|
| 457 |
+
"""Prossegue para Seção 2 sem coordenadas completas (decisão explícita do usuário)."""
|
| 458 |
+
if df is None:
|
| 459 |
+
return (
|
| 460 |
+
gr.update(), gr.update(), gr.update(),
|
| 461 |
+
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
| 462 |
+
"Sem dados carregados.",
|
| 463 |
+
)
|
| 464 |
+
mapa = criar_mapa(df)
|
| 465 |
+
return _revelar_secao_2(df, mapa) + ("Seção 2 liberada sem coordenadas completas.",)
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
# ============================================================
|
| 469 |
+
# INTERFACE GRADIO
|
| 470 |
+
# ============================================================
|
| 471 |
+
|
| 472 |
+
def criar_aba():
|
| 473 |
+
"""Cria conteúdo da aba de elaboração (sem wrapper gr.Blocks)."""
|
| 474 |
+
|
| 475 |
+
# Estados
|
| 476 |
+
estado_df = gr.State(None)
|
| 477 |
+
estado_modelo = gr.State(None)
|
| 478 |
+
estado_estatisticas = gr.State(None)
|
| 479 |
+
estado_outliers_anteriores = gr.State([]) # Lista de índices excluídos em iterações anteriores
|
| 480 |
+
estado_iteracao = gr.State(1) # Contador de iterações
|
| 481 |
+
estado_avaliacoes = gr.State([]) # Lista de avaliações acumuladas
|
| 482 |
+
|
| 483 |
+
# ========================================
|
| 484 |
+
# SEÇÃO 1: IMPORTAR DADOS (sempre visível e aberta)
|
| 485 |
+
# ========================================
|
| 486 |
+
# Estado para armazenar o arquivo temporariamente (usado quando há múltiplas abas)
|
| 487 |
+
estado_arquivo_temp = gr.State(None)
|
| 488 |
+
estado_flag_carregamento = gr.State(False)
|
| 489 |
+
estado_geo_temp = gr.State(None) # DataFrame durante geocodificação (antes de confirmar)
|
| 490 |
+
estado_df_falhas = gr.State(None) # df_falhas mais recente (geocodificação)
|
| 491 |
+
|
| 492 |
+
header_secao_1 = gr.HTML(criar_header_secao(1, "Importar Dados"))
|
| 493 |
+
with gr.Accordion(
|
| 494 |
+
label="▼ Mostrar / Ocultar",
|
| 495 |
+
open=True,
|
| 496 |
+
elem_classes="section-accordion"
|
| 497 |
+
) as accordion_secao_1:
|
| 498 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_1:
|
| 499 |
+
# Estado A: Upload (visível inicialmente)
|
| 500 |
+
with gr.Row(visible=True) as row_upload:
|
| 501 |
+
upload = gr.File(
|
| 502 |
+
label="Carregar arquivo (Excel, CSV ou Modelo .dai)",
|
| 503 |
+
scale=2
|
| 504 |
+
)
|
| 505 |
+
status = gr.Textbox(
|
| 506 |
+
label="Status",
|
| 507 |
+
interactive=False,
|
| 508 |
+
scale=2
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
# Estado B: Seleção de aba — apenas para Excel multi-aba (oculto inicialmente)
|
| 512 |
+
with gr.Group(visible=False) as row_selecao_aba:
|
| 513 |
+
with gr.Row():
|
| 514 |
+
dropdown_aba = gr.Dropdown(
|
| 515 |
+
label="Selecionar Aba",
|
| 516 |
+
choices=[],
|
| 517 |
+
interactive=True,
|
| 518 |
+
)
|
| 519 |
+
with gr.Row():
|
| 520 |
+
btn_confirmar_aba = gr.Button("Selecionar Aba", variant="primary")
|
| 521 |
+
|
| 522 |
+
# Estado C: Pós-carregamento (oculto inicialmente)
|
| 523 |
+
with gr.Row(visible=False) as row_pos_carga:
|
| 524 |
+
with gr.Column(scale=5):
|
| 525 |
+
html_nome_arquivo_carregado = gr.HTML(value="")
|
| 526 |
+
with gr.Column(scale=2):
|
| 527 |
+
btn_reiniciar_app = gr.Button(
|
| 528 |
+
"Reiniciar Aplicação",
|
| 529 |
+
variant="danger"
|
| 530 |
+
)
|
| 531 |
+
html_aviso_reiniciar = gr.HTML(value=(
|
| 532 |
+
'<div style="background-color:#fff3cd; border:1px solid #ffc107; '
|
| 533 |
+
'border-radius:8px; padding:8px 12px; margin-top:8px; font-size:1em;">'
|
| 534 |
+
'<strong>Atenção:</strong> Ao reiniciar, toda a aplicação MESA será '
|
| 535 |
+
'recarregada e <strong>o conteúdo de todas as abas será perdido</strong>. '
|
| 536 |
+
'Para iniciar uma nova sessão MESA sem perder o trabalho atual, '
|
| 537 |
+
'abra uma <strong>nova aba do navegador</strong>.'
|
| 538 |
+
'</div>'
|
| 539 |
+
))
|
| 540 |
+
|
| 541 |
+
# Estado D: Resolução de Coordenadas (oculto inicialmente)
|
| 542 |
+
with gr.Row(visible=False) as row_coords_panel:
|
| 543 |
+
with gr.Column():
|
| 544 |
+
html_aviso_coords = gr.HTML(value="")
|
| 545 |
+
|
| 546 |
+
# Tela de escolha (visível inicialmente)
|
| 547 |
+
with gr.Row() as row_escolha_opcao:
|
| 548 |
+
with gr.Column():
|
| 549 |
+
with gr.Row():
|
| 550 |
+
btn_escolher_mapear = gr.Button(
|
| 551 |
+
"Mapear colunas existentes para lat/lon",
|
| 552 |
+
variant="primary",
|
| 553 |
+
scale=10,
|
| 554 |
+
)
|
| 555 |
+
gr.HTML(
|
| 556 |
+
'<div style="align-self:center;text-align:center;'
|
| 557 |
+
'min-width:36px">'
|
| 558 |
+
'<span style="display:inline-block;font-size:0.75rem;'
|
| 559 |
+
'font-weight:600;color:#9ca3af;letter-spacing:0.05em;'
|
| 560 |
+
'text-transform:uppercase;background:#f3f4f6;'
|
| 561 |
+
'border-radius:999px;padding:3px 8px">ou</span>'
|
| 562 |
+
'</div>',
|
| 563 |
+
scale=1,
|
| 564 |
+
)
|
| 565 |
+
btn_escolher_geocodificar = gr.Button(
|
| 566 |
+
"Geocodificar automaticamente",
|
| 567 |
+
variant="primary",
|
| 568 |
+
scale=10,
|
| 569 |
+
)
|
| 570 |
+
gr.HTML(
|
| 571 |
+
'<div style="align-self:center;text-align:center;'
|
| 572 |
+
'min-width:36px">'
|
| 573 |
+
'<span style="display:inline-block;font-size:0.75rem;'
|
| 574 |
+
'font-weight:600;color:#9ca3af;letter-spacing:0.05em;'
|
| 575 |
+
'text-transform:uppercase;background:#f3f4f6;'
|
| 576 |
+
'border-radius:999px;padding:3px 8px">ou</span>'
|
| 577 |
+
'</div>',
|
| 578 |
+
scale=1,
|
| 579 |
+
)
|
| 580 |
+
btn_prosseguir_sem_coords = gr.Button(
|
| 581 |
+
"Prosseguir sem mapear coordenadas",
|
| 582 |
+
variant="primary",
|
| 583 |
+
scale=10,
|
| 584 |
+
)
|
| 585 |
+
with gr.Row(visible=False) as row_confirmacao_prosseguir:
|
| 586 |
+
with gr.Column():
|
| 587 |
+
gr.HTML(
|
| 588 |
+
'<div style="background:#fff3cd;border:1px solid #ffc107;'
|
| 589 |
+
'border-radius:8px;padding:8px 12px;margin-top:4px">'
|
| 590 |
+
'<strong>Atenção:</strong> Funcionalidades dependentes de '
|
| 591 |
+
'geolocalização (mapa, análises espaciais) não funcionarão '
|
| 592 |
+
'para registros sem coordenadas ou apresentarão resultados '
|
| 593 |
+
'incompletos. Deseja prosseguir mesmo assim?'
|
| 594 |
+
'</div>'
|
| 595 |
+
)
|
| 596 |
+
btn_confirmar_prosseguir = gr.Button(
|
| 597 |
+
"Confirmar — prosseguir mesmo assim",
|
| 598 |
+
variant="stop",
|
| 599 |
+
)
|
| 600 |
+
|
| 601 |
+
# Painel Mapear (oculto — botão Voltar no topo)
|
| 602 |
+
with gr.Row(visible=False) as row_opcao_mapear:
|
| 603 |
+
with gr.Column():
|
| 604 |
+
btn_voltar_mapear = gr.Button("← Voltar", variant="secondary", size="sm")
|
| 605 |
+
with gr.Row():
|
| 606 |
+
dropdown_col_lat_manual = gr.Dropdown(
|
| 607 |
+
label="Coluna → lat",
|
| 608 |
+
choices=[],
|
| 609 |
+
interactive=True,
|
| 610 |
+
)
|
| 611 |
+
dropdown_col_lon_manual = gr.Dropdown(
|
| 612 |
+
label="Coluna → lon",
|
| 613 |
+
choices=[],
|
| 614 |
+
interactive=True,
|
| 615 |
+
)
|
| 616 |
+
html_erro_mapeamento = gr.HTML(value="")
|
| 617 |
+
btn_confirmar_mapeamento = gr.Button(
|
| 618 |
+
"Confirmar mapeamento",
|
| 619 |
+
variant="primary",
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
# Painel Geocodificar (oculto — botão Voltar no topo)
|
| 623 |
+
with gr.Row(visible=False) as row_opcao_geocodificar:
|
| 624 |
+
with gr.Column():
|
| 625 |
+
btn_voltar_geocodificar = gr.Button("← Voltar", variant="secondary", size="sm")
|
| 626 |
+
with gr.Row():
|
| 627 |
+
dropdown_cdlog_geo = gr.Dropdown(
|
| 628 |
+
label="Coluna CDLOG (código do logradouro)",
|
| 629 |
+
choices=[],
|
| 630 |
+
interactive=True,
|
| 631 |
+
)
|
| 632 |
+
dropdown_num_geo = gr.Dropdown(
|
| 633 |
+
label="Coluna Número Predial",
|
| 634 |
+
choices=[],
|
| 635 |
+
interactive=True,
|
| 636 |
+
)
|
| 637 |
+
checkbox_auto_200 = gr.Checkbox(
|
| 638 |
+
label=(
|
| 639 |
+
"Permitir correção automática de ofício para números cuja "
|
| 640 |
+
"diferença do intervalo válido mais próximo seja ≤ 200"
|
| 641 |
+
),
|
| 642 |
+
value=True,
|
| 643 |
+
)
|
| 644 |
+
btn_geocodificar = gr.Button("Geocodificar", variant="primary")
|
| 645 |
+
html_geo_status = gr.HTML(value="<div style='min-height:40px'></div>")
|
| 646 |
+
|
| 647 |
+
# 20 slots de correção individual (ocultos inicialmente)
|
| 648 |
+
falha_rows = []
|
| 649 |
+
falha_htmls = []
|
| 650 |
+
falha_inputs = []
|
| 651 |
+
for i in range(MAX_FALHAS_GEO):
|
| 652 |
+
with gr.Row(visible=False) as _fr:
|
| 653 |
+
with gr.Column(scale=3):
|
| 654 |
+
_fh = gr.HTML(value="")
|
| 655 |
+
with gr.Column(scale=1):
|
| 656 |
+
_fi = gr.Number(label="Nº Corrigido", precision=0, value=None)
|
| 657 |
+
falha_rows.append(_fr)
|
| 658 |
+
falha_htmls.append(_fh)
|
| 659 |
+
falha_inputs.append(_fi)
|
| 660 |
+
|
| 661 |
+
btn_aplicar_correcoes_geo = gr.Button(
|
| 662 |
+
"Aplicar correções e re-geocodificar",
|
| 663 |
+
visible=False,
|
| 664 |
+
)
|
| 665 |
+
btn_usar_coords_geo = gr.Button(
|
| 666 |
+
"Confirmar",
|
| 667 |
+
variant="primary",
|
| 668 |
+
visible=False,
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
# ========================================
|
| 673 |
+
# SEÇÃO 2: VISUALIZAR DADOS (ativada ao carregar arquivo)
|
| 674 |
+
# ========================================
|
| 675 |
+
header_secao_2 = gr.HTML(criar_header_secao(2, "Visualizar Dados"), visible=True)
|
| 676 |
+
with gr.Accordion(
|
| 677 |
+
label="▼ Mostrar / Ocultar",
|
| 678 |
+
open=True,
|
| 679 |
+
visible=False,
|
| 680 |
+
elem_classes="section-accordion"
|
| 681 |
+
) as accordion_secao_2:
|
| 682 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_2:
|
| 683 |
+
with gr.Accordion("Outliers Excluídos", open=True, visible=False) as accordion_outliers_anteriores:
|
| 684 |
+
html_outliers_anteriores = gr.HTML(value="")
|
| 685 |
+
btn_limpar_historico = gr.Button("Limpar Histórico e Reiniciar do Zero", variant="secondary")
|
| 686 |
+
|
| 687 |
+
with gr.Accordion("Dados de Mercado", open=True):
|
| 688 |
+
tabela_dados = gr.Dataframe(
|
| 689 |
+
label="",
|
| 690 |
+
interactive=False,
|
| 691 |
+
max_height=400
|
| 692 |
+
)
|
| 693 |
+
with gr.Row():
|
| 694 |
+
btn_download_dados = gr.Button("Baixar Dados (CSV)", variant="secondary", size="sm")
|
| 695 |
+
download_dados_file = gr.File(label="", visible=False)
|
| 696 |
+
|
| 697 |
+
dropdown_mapa_var = gr.Dropdown(
|
| 698 |
+
label="Variável para dimensionar pontos no mapa",
|
| 699 |
+
choices=["Visualização Padrão"],
|
| 700 |
+
value="Visualização Padrão",
|
| 701 |
+
interactive=True,
|
| 702 |
+
allow_custom_value=False
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
with gr.Accordion("Mapa", open=True):
|
| 706 |
+
mapa_html = gr.HTML(
|
| 707 |
+
value="<p>Carregue dados para ver o mapa.</p>",
|
| 708 |
+
label=""
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
# ========================================
|
| 712 |
+
# SEÇÃO 3: SELECIONAR VARIÁVEL DEPENDENTE (ativada ao carregar arquivo)
|
| 713 |
+
# ========================================
|
| 714 |
+
header_secao_3 = gr.HTML(criar_header_secao(3, "Selecionar Variável Dependente"), visible=True)
|
| 715 |
+
with gr.Accordion(
|
| 716 |
+
label="▼ Mostrar / Ocultar",
|
| 717 |
+
open=True,
|
| 718 |
+
visible=False,
|
| 719 |
+
elem_classes="section-accordion"
|
| 720 |
+
) as accordion_secao_3:
|
| 721 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_3:
|
| 722 |
+
with gr.Row():
|
| 723 |
+
dropdown_y = gr.Dropdown(
|
| 724 |
+
label="Variável Dependente (y)",
|
| 725 |
+
choices=[],
|
| 726 |
+
interactive=True,
|
| 727 |
+
scale=2
|
| 728 |
+
)
|
| 729 |
+
with gr.Row():
|
| 730 |
+
btn_aplicar_y = gr.Button("Aplicar Seleção", variant="primary", scale=1)
|
| 731 |
+
|
| 732 |
+
# ========================================
|
| 733 |
+
# SEÇÃO 4: SELECIONAR VARIÁVEIS INDEPENDENTES (ativada ao selecionar Y)
|
| 734 |
+
# ========================================
|
| 735 |
+
header_secao_4 = gr.HTML(criar_header_secao(4, "Selecionar Variáveis Independentes"), visible=True)
|
| 736 |
+
with gr.Accordion(
|
| 737 |
+
label="▼ Mostrar / Ocultar",
|
| 738 |
+
open=True,
|
| 739 |
+
visible=False,
|
| 740 |
+
elem_classes="section-accordion"
|
| 741 |
+
) as accordion_secao_4:
|
| 742 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_4:
|
| 743 |
+
with gr.Row(elem_classes="checkbox-selecionar-todos"):
|
| 744 |
+
checkbox_selecionar_todos = gr.Checkbox(
|
| 745 |
+
label="Marcar ou desmarcar todas as variáveis",
|
| 746 |
+
value=True,
|
| 747 |
+
interactive=True
|
| 748 |
+
)
|
| 749 |
+
with gr.Row():
|
| 750 |
+
checkboxes_x = gr.CheckboxGroup(
|
| 751 |
+
label="Variáveis Independentes (X)",
|
| 752 |
+
choices=[],
|
| 753 |
+
interactive=True
|
| 754 |
+
)
|
| 755 |
+
with gr.Row():
|
| 756 |
+
checkboxes_dicotomicas = gr.CheckboxGroup(
|
| 757 |
+
label="Variáveis Dicotômicas (0/1)",
|
| 758 |
+
choices=[], value=[], visible=False, interactive=True,
|
| 759 |
+
info="Transformação fixa em (x). Avaliação: apenas 0 ou 1."
|
| 760 |
+
)
|
| 761 |
+
with gr.Row():
|
| 762 |
+
checkboxes_codigo_alocado = gr.CheckboxGroup(
|
| 763 |
+
label="Variáveis de Código Alocado/Ajustado",
|
| 764 |
+
choices=[], value=[], visible=False, interactive=True,
|
| 765 |
+
info="Transformação livre. Avaliação: apenas inteiros no intervalo observado."
|
| 766 |
+
)
|
| 767 |
+
with gr.Row():
|
| 768 |
+
checkboxes_percentuais = gr.CheckboxGroup(
|
| 769 |
+
label="Variáveis Percentuais (0 a 1)",
|
| 770 |
+
choices=[], value=[], visible=False, interactive=True,
|
| 771 |
+
info="Transformação fixa em (x). Avaliação: apenas valores entre 0 e 1."
|
| 772 |
+
)
|
| 773 |
+
with gr.Row():
|
| 774 |
+
btn_aplicar_selecao_x = gr.Button("Aplicar Seleção", variant="primary", scale=1)
|
| 775 |
+
html_aviso_multicolinearidade = gr.HTML("", visible=False)
|
| 776 |
+
|
| 777 |
+
# Estados para outliers (usados na seção de Análise de Outliers após o modelo)
|
| 778 |
+
estado_metricas = gr.State(None)
|
| 779 |
+
estado_df_filtrado = gr.State(None)
|
| 780 |
+
|
| 781 |
+
# ========================================
|
| 782 |
+
# SEÇÃO 5: ESTATÍSTICAS DAS VARIÁVEIS SELECIONADAS (ativada por Aplicar Seleção)
|
| 783 |
+
# ========================================
|
| 784 |
+
header_secao_5 = gr.HTML(criar_header_secao(5, "Estatísticas das Variáveis Selecionadas"), visible=True)
|
| 785 |
+
with gr.Accordion(
|
| 786 |
+
label="▼ Mostrar / Ocultar",
|
| 787 |
+
open=True,
|
| 788 |
+
visible=False,
|
| 789 |
+
elem_classes="section-accordion"
|
| 790 |
+
) as accordion_secao_5:
|
| 791 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_5:
|
| 792 |
+
tabela_estatisticas = gr.Dataframe(
|
| 793 |
+
label="",
|
| 794 |
+
interactive=False,
|
| 795 |
+
value=None,
|
| 796 |
+
wrap=True
|
| 797 |
+
)
|
| 798 |
+
with gr.Row():
|
| 799 |
+
btn_download_estatisticas = gr.Button("Baixar Estatísticas (CSV)", variant="secondary", size="sm")
|
| 800 |
+
download_estatisticas_file = gr.File(label="", visible=False)
|
| 801 |
+
|
| 802 |
+
# ========================================
|
| 803 |
+
# SEÇÃO 6: TESTE DE MICRONUMEROSIDADE (ativada por Aplicar Seleção)
|
| 804 |
+
# ========================================
|
| 805 |
+
header_secao_6 = gr.HTML(criar_header_secao(6, "Teste de Micronumerosidade"), visible=True)
|
| 806 |
+
with gr.Accordion(
|
| 807 |
+
label="▼ Mostrar / Ocultar",
|
| 808 |
+
open=True,
|
| 809 |
+
visible=False,
|
| 810 |
+
elem_classes="section-accordion"
|
| 811 |
+
) as accordion_secao_6:
|
| 812 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_6:
|
| 813 |
+
html_micronumerosidade = gr.HTML(value="")
|
| 814 |
+
|
| 815 |
+
# ========================================
|
| 816 |
+
# SEÇÃO 7: GRÁFICOS DE DISPERSÃO (ativada por Aplicar Seleção)
|
| 817 |
+
# ========================================
|
| 818 |
+
header_secao_7 = gr.HTML(criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes"), visible=True)
|
| 819 |
+
with gr.Accordion(
|
| 820 |
+
label="▼ Mostrar / Ocultar",
|
| 821 |
+
open=True,
|
| 822 |
+
visible=False,
|
| 823 |
+
elem_classes="section-accordion"
|
| 824 |
+
) as accordion_secao_7:
|
| 825 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_7:
|
| 826 |
+
plot_dispersao = gr.Plot(label="")
|
| 827 |
+
|
| 828 |
+
# ========================================
|
| 829 |
+
# SEÇÃO 8: TRANSFORMAÇÕES SUGERIDAS (ativada por Aplicar Seleção)
|
| 830 |
+
# ========================================
|
| 831 |
+
# Estado para armazenar resultados da busca
|
| 832 |
+
estado_resultados_busca = gr.State([])
|
| 833 |
+
|
| 834 |
+
header_secao_8 = gr.HTML(criar_header_secao(8, "Transformações Sugeridas"), visible=True)
|
| 835 |
+
with gr.Accordion(
|
| 836 |
+
label="▼ Mostrar / Ocultar",
|
| 837 |
+
open=True,
|
| 838 |
+
visible=False,
|
| 839 |
+
elem_classes="section-accordion"
|
| 840 |
+
) as accordion_secao_8:
|
| 841 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_8:
|
| 842 |
+
with gr.Row():
|
| 843 |
+
slider_grau_coef = gr.Radio(
|
| 844 |
+
choices=[("Sem enquadramento", 0), ("Grau I (p≤30%)", 1), ("Grau II (p≤20%)", 2), ("Grau III (p≤10%)", 3)],
|
| 845 |
+
value=3,
|
| 846 |
+
label="Grau mínimo de significância dos coeficientes",
|
| 847 |
+
type="value"
|
| 848 |
+
)
|
| 849 |
+
slider_grau_f = gr.Radio(
|
| 850 |
+
choices=[("Sem enquadramento", 0), ("Grau I (α=5%)", 1), ("Grau II (α=2%)", 2), ("Grau III (α=1%)", 3)],
|
| 851 |
+
value=3,
|
| 852 |
+
label="Grau mínimo do Teste F",
|
| 853 |
+
type="value"
|
| 854 |
+
)
|
| 855 |
+
busca_html = gr.HTML(value="")
|
| 856 |
+
|
| 857 |
+
# ========================================
|
| 858 |
+
# SEÇÃO 9: APLICAÇÃO DAS TRANSFORMAÇÕES (ativada por Aplicar Seleção)
|
| 859 |
+
# ========================================
|
| 860 |
+
header_secao_9 = gr.HTML(criar_header_secao(9, "Aplicação das Transformações"), visible=True)
|
| 861 |
+
with gr.Accordion(
|
| 862 |
+
label="▼ Mostrar / Ocultar",
|
| 863 |
+
open=True,
|
| 864 |
+
visible=False,
|
| 865 |
+
elem_classes="section-accordion"
|
| 866 |
+
) as accordion_secao_9:
|
| 867 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_9:
|
| 868 |
+
# Área destacada para botões de adoção
|
| 869 |
+
gr.HTML("""
|
| 870 |
+
<div class="adotar-header">
|
| 871 |
+
<span class="adotar-titulo">Adote uma das otimizações de transformação sugeridas na seção anterior</span>
|
| 872 |
+
</div>
|
| 873 |
+
""")
|
| 874 |
+
with gr.Row(elem_classes="adotar-row"):
|
| 875 |
+
btn_adotar_1 = gr.Button("✓ Adotar #1", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 876 |
+
btn_adotar_2 = gr.Button("✓ Adotar #2", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 877 |
+
btn_adotar_3 = gr.Button("✓ Adotar #3", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 878 |
+
btn_adotar_4 = gr.Button("✓ Adotar #4", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 879 |
+
btn_adotar_5 = gr.Button("✓ Adotar #5", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 880 |
+
|
| 881 |
+
gr.HTML("<div class='adotar-separator'><span>ou configure manualmente</span></div>")
|
| 882 |
+
|
| 883 |
+
gr.Markdown("*Selecione a transformação para cada variável (dicotômicas ficam fixas em (x))*", elem_classes="transf-instrucao")
|
| 884 |
+
|
| 885 |
+
# Cards: os 3 gr.Row são transparentes (display:contents via CSS) e fluem
|
| 886 |
+
# num único container flex (.transf-all-cards), garantindo distribuição uniforme
|
| 887 |
+
# 3 linhas × 8 = 24 slots; apenas os 20 primeiros são rastreados
|
| 888 |
+
# Card Y fica antes dos cards X (mesmo container flex)
|
| 889 |
+
transf_x_rows = []
|
| 890 |
+
transf_x_columns = []
|
| 891 |
+
transf_x_labels = []
|
| 892 |
+
transf_x_dropdowns = []
|
| 893 |
+
|
| 894 |
+
with gr.Column(elem_classes="transf-all-cards"):
|
| 895 |
+
# Card da variável dependente Y (borda azul, label dinâmico com nome + "(Y)")
|
| 896 |
+
with gr.Row(visible=False, elem_classes="transf-cards-row") as transf_y_row:
|
| 897 |
+
with gr.Column(scale=1, min_width=110, elem_classes="transf-card transf-card-y", visible=False) as transf_y_col:
|
| 898 |
+
transf_y_label = gr.HTML(value="", visible=False, elem_classes="transf-card-label")
|
| 899 |
+
transformacao_y = gr.Dropdown(
|
| 900 |
+
choices=TRANSFORMACOES,
|
| 901 |
+
value="(x)",
|
| 902 |
+
label="",
|
| 903 |
+
show_label=False,
|
| 904 |
+
interactive=True,
|
| 905 |
+
visible=True,
|
| 906 |
+
)
|
| 907 |
+
|
| 908 |
+
for i in range((MAX_VARS_X + 7) // 8): # 3 rows
|
| 909 |
+
row = gr.Row(visible=False, elem_classes="transf-cards-row")
|
| 910 |
+
with row:
|
| 911 |
+
for j in range(8):
|
| 912 |
+
k = i * 8 + j
|
| 913 |
+
col = gr.Column(scale=1, min_width=110, elem_classes="transf-card", visible=False)
|
| 914 |
+
with col:
|
| 915 |
+
label = gr.HTML(value="", visible=False, elem_classes="transf-card-label")
|
| 916 |
+
dropdown = gr.Dropdown(
|
| 917 |
+
choices=TRANSFORMACOES,
|
| 918 |
+
value="(x)",
|
| 919 |
+
label="",
|
| 920 |
+
show_label=False,
|
| 921 |
+
interactive=True,
|
| 922 |
+
visible=False,
|
| 923 |
+
)
|
| 924 |
+
if k < MAX_VARS_X:
|
| 925 |
+
transf_x_columns.append(col)
|
| 926 |
+
transf_x_labels.append(label)
|
| 927 |
+
transf_x_dropdowns.append(dropdown)
|
| 928 |
+
transf_x_rows.append(row)
|
| 929 |
+
|
| 930 |
+
with gr.Row():
|
| 931 |
+
btn_ajustar = gr.Button("Aplicar transformações e ajustar modelo", variant="primary", scale=1)
|
| 932 |
+
|
| 933 |
+
# ========================================
|
| 934 |
+
# SEÇÃO 10: GRÁFICOS DE DISPERSÃO (VARIÁVEIS TRANSFORMADAS) (ativada por Ajustar Modelo)
|
| 935 |
+
# ========================================
|
| 936 |
+
header_secao_10 = gr.HTML(criar_header_secao(10, "Gráficos de Dispersão (Variáveis Transformadas)"), visible=True)
|
| 937 |
+
with gr.Accordion(
|
| 938 |
+
label="▼ Mostrar / Ocultar",
|
| 939 |
+
open=True,
|
| 940 |
+
visible=False,
|
| 941 |
+
elem_classes="section-accordion"
|
| 942 |
+
) as accordion_secao_10:
|
| 943 |
+
with gr.Column(elem_classes="section-content") as conteudo_secao_10:
|
| 944 |
+
dropdown_tipo_grafico_dispersao = gr.Dropdown(
|
| 945 |
+
choices=[
|
| 946 |
+
"Variáveis Independentes Transformadas X Variável Dependente Transformada",
|
| 947 |
+
"Variáveis Independentes Transformadas X Resíduo Padronizado"
|
| 948 |
+
],
|
| 949 |
+
value="Variáveis Independentes Transformadas X Variável Dependente Transformada",
|
| 950 |
+
label="Tipo de Gráfico",
|
| 951 |
+
interactive=True,
|
| 952 |
+
visible=True,
|
| 953 |
+
elem_id="dropdown_dispersao"
|
| 954 |
+
)
|
| 955 |
+
plot_dispersao_transf = gr.Plot(label="", visible=True)
|
| 956 |
+
|
| 957 |
+
# ========================================
|
| 958 |
+
# SEÇÃO 11: DIAGNÓSTICO DE MODELO (ativada por Ajustar Modelo)
|
| 959 |
+
# ========================================
|
| 960 |
+
header_secao_11 = gr.HTML(criar_header_secao(11, "Diagnóstico de Modelo"), visible=True)
|
| 961 |
+
with gr.Accordion(
|
| 962 |
+
label="▼ Mostrar / Ocultar",
|
| 963 |
+
open=True,
|
| 964 |
+
visible=False,
|
| 965 |
+
elem_classes="section-accordion"
|
| 966 |
+
) as accordion_secao_11:
|
| 967 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_11:
|
| 968 |
+
diagnosticos_html = gr.HTML(value="")
|
| 969 |
+
|
| 970 |
+
with gr.Row():
|
| 971 |
+
with gr.Accordion("Tabela de Coeficientes", open=True):
|
| 972 |
+
tabela_coef = gr.Dataframe(
|
| 973 |
+
label="",
|
| 974 |
+
interactive=False,
|
| 975 |
+
max_height=400
|
| 976 |
+
)
|
| 977 |
+
with gr.Row():
|
| 978 |
+
btn_download_coef = gr.Button("Baixar Coeficientes (CSV)", variant="secondary", size="sm")
|
| 979 |
+
download_coef_file = gr.File(label="", visible=False)
|
| 980 |
+
|
| 981 |
+
with gr.Accordion("Valores Observados vs Calculados", open=True):
|
| 982 |
+
tabela_obs_calc = gr.Dataframe(
|
| 983 |
+
label="",
|
| 984 |
+
interactive=False,
|
| 985 |
+
max_height=400
|
| 986 |
+
)
|
| 987 |
+
with gr.Row():
|
| 988 |
+
btn_download_obs_calc = gr.Button("Baixar Obs vs Calc (CSV)", variant="secondary", size="sm")
|
| 989 |
+
download_obs_calc_file = gr.File(label="", visible=False)
|
| 990 |
+
|
| 991 |
+
# ========================================
|
| 992 |
+
# SEÇÃO 12: GRÁFICOS DE DIAGNÓSTICO DO MODELO (ativada por Ajustar Modelo)
|
| 993 |
+
# ========================================
|
| 994 |
+
header_secao_12 = gr.HTML(criar_header_secao(12, "Gráficos de Diagnóstico do Modelo"), visible=True)
|
| 995 |
+
with gr.Accordion(
|
| 996 |
+
label="▼ Mostrar / Ocultar",
|
| 997 |
+
open=True,
|
| 998 |
+
visible=False,
|
| 999 |
+
elem_classes="section-accordion"
|
| 1000 |
+
) as accordion_secao_12:
|
| 1001 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_12:
|
| 1002 |
+
with gr.Row():
|
| 1003 |
+
plot_obs_calc = gr.Plot(label="Observados vs Calculados")
|
| 1004 |
+
plot_residuos = gr.Plot(label="Resíduos")
|
| 1005 |
+
|
| 1006 |
+
with gr.Row():
|
| 1007 |
+
plot_hist = gr.Plot(label="Histograma dos Resíduos")
|
| 1008 |
+
plot_cook = gr.Plot(label="Distância de Cook")
|
| 1009 |
+
|
| 1010 |
+
with gr.Row():
|
| 1011 |
+
plot_corr = gr.Plot(label="Matriz de Correlação")
|
| 1012 |
+
|
| 1013 |
+
# ========================================
|
| 1014 |
+
# SEÇÃO 13: ANALISAR OUTLIERS (ativada por Ajustar Modelo)
|
| 1015 |
+
# ========================================
|
| 1016 |
+
# Estado para armazenar o número de filtros visíveis
|
| 1017 |
+
estado_n_filtros = gr.State(2) # Começa com 2 filtros padrão
|
| 1018 |
+
|
| 1019 |
+
# Opções de operadores
|
| 1020 |
+
OPERADORES = ["<=", ">=", "<", ">", "="]
|
| 1021 |
+
# Opções base de variáveis (serão atualizadas dinamicamente)
|
| 1022 |
+
VARIAVEIS_BASE = ["Resíduo Pad.", "Resíduo Stud.", "Cook"]
|
| 1023 |
+
|
| 1024 |
+
header_secao_13 = gr.HTML(criar_header_secao(13, "Analisar Outliers"), visible=True)
|
| 1025 |
+
with gr.Accordion(
|
| 1026 |
+
label="▼ Mostrar / Ocultar",
|
| 1027 |
+
open=True,
|
| 1028 |
+
visible=False,
|
| 1029 |
+
elem_classes="section-accordion"
|
| 1030 |
+
) as accordion_secao_13:
|
| 1031 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_13:
|
| 1032 |
+
gr.HTML('<div class="outlier-subheader">Métricas de Outliers</div>')
|
| 1033 |
+
gr.HTML('<div class="outlier-dica">Métricas calculadas com base no modelo ajustado (resíduos com transformações aplicadas)</div>')
|
| 1034 |
+
|
| 1035 |
+
tabela_metricas = gr.Dataframe(
|
| 1036 |
+
label="Métricas para identificação de outliers",
|
| 1037 |
+
interactive=False,
|
| 1038 |
+
max_height=300
|
| 1039 |
+
)
|
| 1040 |
+
with gr.Row():
|
| 1041 |
+
btn_download_metricas = gr.Button("Baixar Métricas (CSV)", variant="secondary", size="sm")
|
| 1042 |
+
download_metricas_file = gr.File(label="", visible=False)
|
| 1043 |
+
|
| 1044 |
+
# ========================================
|
| 1045 |
+
# SEÇÃO 14: EXCLUSÃO DE OUTLIERS (ativada por Ajustar Modelo)
|
| 1046 |
+
# ========================================
|
| 1047 |
+
header_secao_14 = gr.HTML(criar_header_secao(14, "Exclusão ou Reinclusão de Outliers"), visible=True)
|
| 1048 |
+
with gr.Accordion(
|
| 1049 |
+
label="▼ Mostrar / Ocultar",
|
| 1050 |
+
open=True,
|
| 1051 |
+
visible=False,
|
| 1052 |
+
elem_classes="section-accordion"
|
| 1053 |
+
) as accordion_secao_14:
|
| 1054 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_14:
|
| 1055 |
+
html_outliers_sec14 = gr.HTML(value="")
|
| 1056 |
+
gr.HTML('<div class="outlier-subheader">Filtrar Outliers</div>')
|
| 1057 |
+
gr.HTML('<div class="outlier-dica">Outliers = linhas que satisfazem QUALQUER filtro (lógica OR / União)</div>')
|
| 1058 |
+
|
| 1059 |
+
# Filtros dinâmicos (máximo 4 filtros)
|
| 1060 |
+
filtro_rows = []
|
| 1061 |
+
filtro_vars = []
|
| 1062 |
+
filtro_ops = []
|
| 1063 |
+
filtro_vals = []
|
| 1064 |
+
|
| 1065 |
+
for i in range(4):
|
| 1066 |
+
visible = i < 2 # Filtros 1 e 2 visíveis por padrão
|
| 1067 |
+
# Só define valores padrão para os filtros visíveis (0 e 1)
|
| 1068 |
+
# Filtros ocultos (2 e 3) começam com None para não serem aplicados
|
| 1069 |
+
if i == 0:
|
| 1070 |
+
valor_padrao = -2.0
|
| 1071 |
+
operador_padrao = "<="
|
| 1072 |
+
var_padrao = "Resíduo Pad."
|
| 1073 |
+
elif i == 1:
|
| 1074 |
+
valor_padrao = 2.0
|
| 1075 |
+
operador_padrao = ">="
|
| 1076 |
+
var_padrao = "Resíduo Pad."
|
| 1077 |
+
else:
|
| 1078 |
+
valor_padrao = None
|
| 1079 |
+
operador_padrao = None
|
| 1080 |
+
var_padrao = None
|
| 1081 |
+
|
| 1082 |
+
with gr.Row(visible=visible, elem_classes="filtro-row") as row:
|
| 1083 |
+
var_dropdown = gr.Dropdown(
|
| 1084 |
+
label=f"Variável {i+1}",
|
| 1085 |
+
choices=VARIAVEIS_BASE,
|
| 1086 |
+
value=var_padrao,
|
| 1087 |
+
scale=2
|
| 1088 |
+
)
|
| 1089 |
+
op_dropdown = gr.Dropdown(
|
| 1090 |
+
label="Operador",
|
| 1091 |
+
choices=OPERADORES,
|
| 1092 |
+
value=operador_padrao,
|
| 1093 |
+
scale=1
|
| 1094 |
+
)
|
| 1095 |
+
val_input = gr.Number(
|
| 1096 |
+
label="Valor",
|
| 1097 |
+
value=valor_padrao,
|
| 1098 |
+
scale=1
|
| 1099 |
+
)
|
| 1100 |
+
|
| 1101 |
+
filtro_rows.append(row)
|
| 1102 |
+
filtro_vars.append(var_dropdown)
|
| 1103 |
+
filtro_ops.append(op_dropdown)
|
| 1104 |
+
filtro_vals.append(val_input)
|
| 1105 |
+
|
| 1106 |
+
with gr.Row(elem_classes="btn-filtro-acao-row"):
|
| 1107 |
+
btn_adicionar_filtro = gr.Button("+", variant="secondary", scale=0, min_width=50, elem_classes="btn-filtro-acao btn-adicionar-filtro")
|
| 1108 |
+
btn_remover_ultimo = gr.Button("−", variant="secondary", scale=0, min_width=50, elem_classes="btn-filtro-acao btn-remover-filtro")
|
| 1109 |
+
btn_resetar_filtros = gr.Button("↺", variant="secondary", scale=0, min_width=50, elem_classes="btn-filtro-acao btn-voltar-padrao")
|
| 1110 |
+
|
| 1111 |
+
with gr.Row():
|
| 1112 |
+
btn_aplicar_filtro = gr.Button("Aplicar Filtros", variant="primary", scale=1)
|
| 1113 |
+
|
| 1114 |
+
gr.HTML('<div class="outlier-divider"><span class="arrow">▼</span></div>')
|
| 1115 |
+
gr.HTML('<div class="outlier-subheader">Confirmar Filtros Selecionados ou Ajustar Manualmente</div>')
|
| 1116 |
+
gr.HTML('<div class="outlier-dica">Edite os índices manualmente ou confirme os resultados dos filtros acima</div>')
|
| 1117 |
+
|
| 1118 |
+
with gr.Row():
|
| 1119 |
+
outliers_texto = gr.Textbox(
|
| 1120 |
+
label="Índices a Excluir",
|
| 1121 |
+
placeholder="Ex: 5, 12, 23",
|
| 1122 |
+
scale=3
|
| 1123 |
+
)
|
| 1124 |
+
reincluir_texto = gr.Textbox(
|
| 1125 |
+
label="Índices a Reincluir",
|
| 1126 |
+
placeholder="Ex: 5, 12",
|
| 1127 |
+
scale=3
|
| 1128 |
+
)
|
| 1129 |
+
|
| 1130 |
+
with gr.Row():
|
| 1131 |
+
txt_resumo_outliers = gr.Textbox(
|
| 1132 |
+
label="Resumo",
|
| 1133 |
+
value="Excluídos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0",
|
| 1134 |
+
interactive=False,
|
| 1135 |
+
elem_classes="resumo-outliers"
|
| 1136 |
+
)
|
| 1137 |
+
|
| 1138 |
+
with gr.Row():
|
| 1139 |
+
btn_reiniciar_iteracao = gr.Button(
|
| 1140 |
+
"Atualizar Modelo (Excluir/Reincluir Outliers)",
|
| 1141 |
+
variant="primary",
|
| 1142 |
+
scale=2,
|
| 1143 |
+
elem_classes="btn-reiniciar-iteracao"
|
| 1144 |
+
)
|
| 1145 |
+
btn_download_base = gr.Button("Baixar Base Tratada (CSV)", variant="secondary", scale=1)
|
| 1146 |
+
download_base_file = gr.File(label="", visible=False)
|
| 1147 |
+
|
| 1148 |
+
# ========================================
|
| 1149 |
+
# SEÇÃO 15: AVALIAÇÃO DE IMÓVEL (ativada por Ajustar Modelo)
|
| 1150 |
+
# ========================================
|
| 1151 |
+
N_COLS_AVAL = 4
|
| 1152 |
+
N_ROWS_AVAL = MAX_VARS_X // N_COLS_AVAL # 5
|
| 1153 |
+
|
| 1154 |
+
header_secao_15 = gr.HTML(criar_header_secao(15, "Avaliação de Imóvel"), visible=True)
|
| 1155 |
+
with gr.Accordion(
|
| 1156 |
+
label="▼ Mostrar / Ocultar",
|
| 1157 |
+
open=True,
|
| 1158 |
+
visible=False,
|
| 1159 |
+
elem_classes="section-accordion"
|
| 1160 |
+
) as accordion_secao_15:
|
| 1161 |
+
with gr.Group(elem_classes="section-content"):
|
| 1162 |
+
# Grid de inputs (5 rows × 4 cols = 20 inputs pré-criados) — visual de cards
|
| 1163 |
+
aval_rows = []
|
| 1164 |
+
aval_inputs = []
|
| 1165 |
+
with gr.Column(elem_classes="aval-all-cards"):
|
| 1166 |
+
for i in range(N_ROWS_AVAL):
|
| 1167 |
+
with gr.Row(visible=False, elem_classes="aval-cards-row") as aval_row:
|
| 1168 |
+
for j in range(N_COLS_AVAL):
|
| 1169 |
+
inp = gr.Number(label="", visible=False, interactive=True, elem_classes="aval-card")
|
| 1170 |
+
aval_inputs.append(inp)
|
| 1171 |
+
aval_rows.append(aval_row)
|
| 1172 |
+
|
| 1173 |
+
with gr.Row():
|
| 1174 |
+
btn_calcular_avaliacao = gr.Button("Calcular Avaliação", variant="primary", scale=2)
|
| 1175 |
+
btn_limpar_avaliacoes = gr.Button("Limpar Avaliações", variant="secondary", scale=1)
|
| 1176 |
+
with gr.Row():
|
| 1177 |
+
dropdown_base_avaliacao = gr.Dropdown(
|
| 1178 |
+
label="Base p/ comparação",
|
| 1179 |
+
choices=[],
|
| 1180 |
+
value=None,
|
| 1181 |
+
interactive=True,
|
| 1182 |
+
scale=1,
|
| 1183 |
+
)
|
| 1184 |
+
|
| 1185 |
+
resultado_avaliacao_html = gr.HTML("")
|
| 1186 |
+
excluir_aval_trigger = gr.Textbox(
|
| 1187 |
+
label="", elem_id="excluir-aval-elab", container=False,
|
| 1188 |
+
elem_classes="trigger-hidden"
|
| 1189 |
+
)
|
| 1190 |
+
|
| 1191 |
+
with gr.Row():
|
| 1192 |
+
btn_exportar_avaliacoes = gr.Button("Salvar Avaliações em Excel", variant="secondary")
|
| 1193 |
+
download_avaliacoes_file = gr.File(label="", visible=False)
|
| 1194 |
+
|
| 1195 |
+
# ========================================
|
| 1196 |
+
# SEÇÃO 16: EXPORTAR MODELO (ativada por Ajustar Modelo)
|
| 1197 |
+
# ========================================
|
| 1198 |
+
header_secao_16 = gr.HTML(criar_header_secao(16, "Exportar Modelo"), visible=True)
|
| 1199 |
+
with gr.Accordion(
|
| 1200 |
+
label="▼ Mostrar / Ocultar",
|
| 1201 |
+
open=True,
|
| 1202 |
+
visible=False,
|
| 1203 |
+
elem_classes="section-accordion"
|
| 1204 |
+
) as accordion_secao_16:
|
| 1205 |
+
with gr.Group(elem_classes="section-content") as conteudo_secao_16:
|
| 1206 |
+
with gr.Row():
|
| 1207 |
+
nome_arquivo = gr.Textbox(
|
| 1208 |
+
label="Nome do arquivo",
|
| 1209 |
+
placeholder="modelo_01",
|
| 1210 |
+
scale=2
|
| 1211 |
+
)
|
| 1212 |
+
dropdown_elaborador = gr.Dropdown(
|
| 1213 |
+
label="Elaborador",
|
| 1214 |
+
choices=_avaliadores_nomes,
|
| 1215 |
+
value=None,
|
| 1216 |
+
scale=2
|
| 1217 |
+
)
|
| 1218 |
+
btn_exportar = gr.Button("Exportar .dai", variant="primary", scale=1)
|
| 1219 |
+
|
| 1220 |
+
status_exportar = gr.Textbox(
|
| 1221 |
+
label="Status da exportação",
|
| 1222 |
+
interactive=False
|
| 1223 |
+
)
|
| 1224 |
+
|
| 1225 |
+
download_modelo_file = gr.File(
|
| 1226 |
+
label="",
|
| 1227 |
+
visible=False
|
| 1228 |
+
)
|
| 1229 |
+
|
| 1230 |
+
# ========================================
|
| 1231 |
+
# EVENTOS
|
| 1232 |
+
# ========================================
|
| 1233 |
+
|
| 1234 |
+
# --- Output lists compartilhadas ---
|
| 1235 |
+
|
| 1236 |
+
# CONTRACT: 196 itens — usada por upload.upload e btn_confirmar_aba.click
|
| 1237 |
+
# Se alterar, atualizar: carregamento.py (5 funções retornam 196 itens)
|
| 1238 |
+
_outputs_carregar = [
|
| 1239 |
+
estado_df,
|
| 1240 |
+
status,
|
| 1241 |
+
dropdown_aba,
|
| 1242 |
+
dropdown_y,
|
| 1243 |
+
tabela_dados,
|
| 1244 |
+
tabela_estatisticas,
|
| 1245 |
+
checkboxes_x,
|
| 1246 |
+
mapa_html,
|
| 1247 |
+
estado_df_filtrado,
|
| 1248 |
+
estado_outliers_anteriores,
|
| 1249 |
+
estado_iteracao,
|
| 1250 |
+
accordion_outliers_anteriores,
|
| 1251 |
+
html_outliers_anteriores,
|
| 1252 |
+
estado_arquivo_temp,
|
| 1253 |
+
# Seções 2 e 3
|
| 1254 |
+
header_secao_2,
|
| 1255 |
+
accordion_secao_2,
|
| 1256 |
+
dropdown_mapa_var,
|
| 1257 |
+
header_secao_3,
|
| 1258 |
+
accordion_secao_3,
|
| 1259 |
+
# Reset seções 4-16 (states)
|
| 1260 |
+
estado_modelo, estado_metricas, estado_resultados_busca, estado_avaliacoes,
|
| 1261 |
+
# Headers, accordions e conteúdo das seções 4-16
|
| 1262 |
+
header_secao_4, accordion_secao_4, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais,
|
| 1263 |
+
html_aviso_multicolinearidade,
|
| 1264 |
+
header_secao_5, accordion_secao_5,
|
| 1265 |
+
header_secao_6, accordion_secao_6, html_micronumerosidade,
|
| 1266 |
+
header_secao_7, accordion_secao_7, plot_dispersao,
|
| 1267 |
+
header_secao_8, accordion_secao_8, slider_grau_coef, slider_grau_f, busca_html,
|
| 1268 |
+
header_secao_9, accordion_secao_9, transformacao_y,
|
| 1269 |
+
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1270 |
+
] + [transf_y_row, transf_y_col, transf_y_label] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [
|
| 1271 |
+
header_secao_10, accordion_secao_10, dropdown_tipo_grafico_dispersao, plot_dispersao_transf,
|
| 1272 |
+
header_secao_11, accordion_secao_11, diagnosticos_html, tabela_coef, tabela_obs_calc,
|
| 1273 |
+
header_secao_12, accordion_secao_12, plot_obs_calc, plot_residuos, plot_hist, plot_cook, plot_corr,
|
| 1274 |
+
header_secao_13, accordion_secao_13, tabela_metricas,
|
| 1275 |
+
header_secao_14, accordion_secao_14, html_outliers_sec14, outliers_texto, reincluir_texto, txt_resumo_outliers,
|
| 1276 |
+
# Seção 15: Avaliação de Imóvel
|
| 1277 |
+
header_secao_15, accordion_secao_15,
|
| 1278 |
+
] + aval_rows + aval_inputs + [
|
| 1279 |
+
resultado_avaliacao_html, dropdown_base_avaliacao, excluir_aval_trigger, download_avaliacoes_file,
|
| 1280 |
+
# Seção 16: Exportar Modelo
|
| 1281 |
+
header_secao_16, accordion_secao_16, nome_arquivo, status_exportar,
|
| 1282 |
+
# Controles de visibilidade da seção 1 (pós-carregamento)
|
| 1283 |
+
row_upload, row_selecao_aba, row_pos_carga, html_nome_arquivo_carregado,
|
| 1284 |
+
# Flag para evitar que dropdown_y.change() sobrescreva valores durante carregamento
|
| 1285 |
+
estado_flag_carregamento,
|
| 1286 |
+
] + filtro_vars + [ # 4 dropdowns de variável dos filtros (choices atualizados no fluxo .dai)
|
| 1287 |
+
# Painel de coordenadas (Seção 1, Estado D) — itens 184-192
|
| 1288 |
+
row_coords_panel, # 184
|
| 1289 |
+
html_aviso_coords, # 185
|
| 1290 |
+
dropdown_col_lat_manual, # 186
|
| 1291 |
+
dropdown_col_lon_manual, # 187
|
| 1292 |
+
dropdown_cdlog_geo, # 188
|
| 1293 |
+
dropdown_num_geo, # 189
|
| 1294 |
+
row_escolha_opcao, # 190
|
| 1295 |
+
row_opcao_mapear, # 191
|
| 1296 |
+
row_opcao_geocodificar, # 192
|
| 1297 |
+
]
|
| 1298 |
+
|
| 1299 |
+
# CONTRACT: 33 itens — usada por btn_ajustar.click e btn_reiniciar_iteracao.then
|
| 1300 |
+
# Se alterar, atualizar: modelo.py (ajustar_modelo_callback retorna 33 itens)
|
| 1301 |
+
_outputs_ajustar = [
|
| 1302 |
+
estado_modelo,
|
| 1303 |
+
diagnosticos_html,
|
| 1304 |
+
tabela_coef,
|
| 1305 |
+
tabela_obs_calc,
|
| 1306 |
+
plot_dispersao_transf,
|
| 1307 |
+
dropdown_tipo_grafico_dispersao,
|
| 1308 |
+
plot_obs_calc,
|
| 1309 |
+
plot_residuos,
|
| 1310 |
+
plot_hist,
|
| 1311 |
+
plot_cook,
|
| 1312 |
+
plot_corr,
|
| 1313 |
+
tabela_metricas,
|
| 1314 |
+
estado_metricas,
|
| 1315 |
+
txt_resumo_outliers,
|
| 1316 |
+
estado_avaliacoes,
|
| 1317 |
+
# Seções 10-16
|
| 1318 |
+
header_secao_10, accordion_secao_10,
|
| 1319 |
+
header_secao_11, accordion_secao_11,
|
| 1320 |
+
header_secao_12, accordion_secao_12,
|
| 1321 |
+
header_secao_13, accordion_secao_13,
|
| 1322 |
+
header_secao_14, accordion_secao_14,
|
| 1323 |
+
header_secao_15, accordion_secao_15,
|
| 1324 |
+
header_secao_16, accordion_secao_16,
|
| 1325 |
+
] + filtro_vars
|
| 1326 |
+
|
| 1327 |
+
# CONTRACT: 27 itens — usada por .then() após ajustar/reiniciar para popular campos de avaliação
|
| 1328 |
+
_outputs_popular_avaliacao = aval_rows + aval_inputs + [resultado_avaliacao_html, dropdown_base_avaliacao]
|
| 1329 |
+
|
| 1330 |
+
# --- Inputs compartilhados para ajustar modelo ---
|
| 1331 |
+
_inputs_ajustar = [
|
| 1332 |
+
estado_df_filtrado, estado_df, dropdown_y, checkboxes_x,
|
| 1333 |
+
transformacao_y, estado_outliers_anteriores, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais
|
| 1334 |
+
] + transf_x_dropdowns
|
| 1335 |
+
|
| 1336 |
+
# Upload de arquivo
|
| 1337 |
+
upload.upload(
|
| 1338 |
+
ao_carregar_arquivo,
|
| 1339 |
+
inputs=[upload],
|
| 1340 |
+
outputs=_outputs_carregar
|
| 1341 |
+
).then(
|
| 1342 |
+
popular_campos_avaliacao_callback,
|
| 1343 |
+
inputs=[estado_modelo, tabela_estatisticas],
|
| 1344 |
+
outputs=_outputs_popular_avaliacao
|
| 1345 |
+
).then(
|
| 1346 |
+
popular_dicotomicas_callback,
|
| 1347 |
+
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1348 |
+
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1349 |
+
)
|
| 1350 |
+
|
| 1351 |
+
# Confirmação de aba (para Excel multi-aba)
|
| 1352 |
+
btn_confirmar_aba.click(
|
| 1353 |
+
confirmar_aba_callback,
|
| 1354 |
+
inputs=[estado_arquivo_temp, dropdown_aba],
|
| 1355 |
+
outputs=_outputs_carregar
|
| 1356 |
+
).then(
|
| 1357 |
+
popular_campos_avaliacao_callback,
|
| 1358 |
+
inputs=[estado_modelo, tabela_estatisticas],
|
| 1359 |
+
outputs=_outputs_popular_avaliacao
|
| 1360 |
+
).then(
|
| 1361 |
+
popular_dicotomicas_callback,
|
| 1362 |
+
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1363 |
+
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1364 |
+
)
|
| 1365 |
+
|
| 1366 |
+
# Reiniciar aplicação (reload da página)
|
| 1367 |
+
btn_reiniciar_app.click(
|
| 1368 |
+
fn=None,
|
| 1369 |
+
inputs=None,
|
| 1370 |
+
outputs=None,
|
| 1371 |
+
js="() => { window.location.reload(); }"
|
| 1372 |
+
)
|
| 1373 |
+
|
| 1374 |
+
# ---- Painel de coordenadas (Seção 1, Estado D) ----
|
| 1375 |
+
|
| 1376 |
+
# Tela de escolha → entrar no painel Mapear
|
| 1377 |
+
btn_escolher_mapear.click(
|
| 1378 |
+
fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 1379 |
+
outputs=[row_escolha_opcao, row_opcao_mapear],
|
| 1380 |
+
)
|
| 1381 |
+
|
| 1382 |
+
# Tela de escolha → entrar no painel Geocodificar
|
| 1383 |
+
btn_escolher_geocodificar.click(
|
| 1384 |
+
fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 1385 |
+
outputs=[row_escolha_opcao, row_opcao_geocodificar],
|
| 1386 |
+
)
|
| 1387 |
+
|
| 1388 |
+
# Voltar do painel Mapear (limpa erro, reseta confirmação, volta à escolha)
|
| 1389 |
+
btn_voltar_mapear.click(
|
| 1390 |
+
fn=lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(value=""), gr.update(visible=False)),
|
| 1391 |
+
outputs=[row_opcao_mapear, row_escolha_opcao, html_erro_mapeamento, row_confirmacao_prosseguir],
|
| 1392 |
+
)
|
| 1393 |
+
|
| 1394 |
+
# Voltar do painel Geocodificar (reset completo do estado de geocodificação — 68 itens)
|
| 1395 |
+
N = MAX_FALHAS_GEO
|
| 1396 |
+
btn_voltar_geocodificar.click(
|
| 1397 |
+
fn=lambda: (
|
| 1398 |
+
None, None, "",
|
| 1399 |
+
*[gr.update(visible=False)] * N,
|
| 1400 |
+
*[gr.update(value="")] * N,
|
| 1401 |
+
*[gr.update(value=None)] * N,
|
| 1402 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 1403 |
+
gr.update(visible=False),
|
| 1404 |
+
gr.update(visible=False), gr.update(visible=True),
|
| 1405 |
+
),
|
| 1406 |
+
outputs=(
|
| 1407 |
+
[estado_geo_temp, estado_df_falhas, html_geo_status]
|
| 1408 |
+
+ falha_rows + falha_htmls + falha_inputs
|
| 1409 |
+
+ [btn_aplicar_correcoes_geo, btn_usar_coords_geo,
|
| 1410 |
+
row_confirmacao_prosseguir,
|
| 1411 |
+
row_opcao_geocodificar, row_escolha_opcao]
|
| 1412 |
+
),
|
| 1413 |
+
)
|
| 1414 |
+
|
| 1415 |
+
# Opção 1: confirmar mapeamento manual de colunas
|
| 1416 |
+
btn_confirmar_mapeamento.click(
|
| 1417 |
+
fn=confirmar_mapeamento_callback,
|
| 1418 |
+
inputs=[estado_df, dropdown_col_lat_manual, dropdown_col_lon_manual],
|
| 1419 |
+
outputs=[
|
| 1420 |
+
html_erro_mapeamento,
|
| 1421 |
+
estado_df, estado_df_filtrado, mapa_html,
|
| 1422 |
+
row_coords_panel, header_secao_2, accordion_secao_2,
|
| 1423 |
+
header_secao_3, accordion_secao_3, status,
|
| 1424 |
+
]
|
| 1425 |
+
)
|
| 1426 |
+
|
| 1427 |
+
# Opção 3: geocodificar por eixos (CONTRACT: 67 itens)
|
| 1428 |
+
_outputs_geo = (
|
| 1429 |
+
[estado_geo_temp, estado_df_falhas, html_geo_status]
|
| 1430 |
+
+ falha_rows + falha_htmls + falha_inputs
|
| 1431 |
+
+ [btn_aplicar_correcoes_geo, btn_usar_coords_geo]
|
| 1432 |
+
)
|
| 1433 |
+
|
| 1434 |
+
btn_geocodificar.click(
|
| 1435 |
+
fn=geocodificar_callback,
|
| 1436 |
+
inputs=[estado_df, dropdown_cdlog_geo, dropdown_num_geo, checkbox_auto_200],
|
| 1437 |
+
outputs=_outputs_geo,
|
| 1438 |
+
show_progress="full",
|
| 1439 |
+
)
|
| 1440 |
+
|
| 1441 |
+
btn_aplicar_correcoes_geo.click(
|
| 1442 |
+
fn=aplicar_correcoes_geo_callback,
|
| 1443 |
+
inputs=(
|
| 1444 |
+
[estado_geo_temp, estado_df_falhas]
|
| 1445 |
+
+ falha_inputs
|
| 1446 |
+
+ [dropdown_cdlog_geo, dropdown_num_geo, checkbox_auto_200]
|
| 1447 |
+
),
|
| 1448 |
+
outputs=_outputs_geo,
|
| 1449 |
+
show_progress="full",
|
| 1450 |
+
)
|
| 1451 |
+
|
| 1452 |
+
btn_usar_coords_geo.click(
|
| 1453 |
+
fn=confirmar_geocodificacao_callback,
|
| 1454 |
+
inputs=[estado_geo_temp],
|
| 1455 |
+
outputs=[
|
| 1456 |
+
estado_df, estado_df_filtrado, mapa_html,
|
| 1457 |
+
row_coords_panel, header_secao_2, accordion_secao_2,
|
| 1458 |
+
header_secao_3, accordion_secao_3, status,
|
| 1459 |
+
]
|
| 1460 |
+
)
|
| 1461 |
+
|
| 1462 |
+
# Prosseguir sem coordenadas completas (dois cliques: exibe aviso → confirma)
|
| 1463 |
+
btn_prosseguir_sem_coords.click(
|
| 1464 |
+
fn=lambda: gr.update(visible=True),
|
| 1465 |
+
outputs=[row_confirmacao_prosseguir]
|
| 1466 |
+
)
|
| 1467 |
+
|
| 1468 |
+
btn_confirmar_prosseguir.click(
|
| 1469 |
+
fn=lambda geo_df, orig_df: confirmar_sem_coords_callback(geo_df if geo_df is not None else orig_df),
|
| 1470 |
+
inputs=[estado_geo_temp, estado_df],
|
| 1471 |
+
outputs=[
|
| 1472 |
+
estado_df, estado_df_filtrado, mapa_html,
|
| 1473 |
+
row_coords_panel, header_secao_2, accordion_secao_2,
|
| 1474 |
+
header_secao_3, accordion_secao_3, status,
|
| 1475 |
+
]
|
| 1476 |
+
)
|
| 1477 |
+
|
| 1478 |
+
# Mudança de y (NÃO mostra seção 4, NÃO atualiza estatísticas - só atualiza checkboxes)
|
| 1479 |
+
dropdown_y.change(
|
| 1480 |
+
ao_mudar_y_sem_estatisticas,
|
| 1481 |
+
inputs=[estado_df, dropdown_y, estado_flag_carregamento],
|
| 1482 |
+
outputs=[checkboxes_x, header_secao_4, accordion_secao_4, estado_flag_carregamento]
|
| 1483 |
+
).then(
|
| 1484 |
+
popular_dicotomicas_callback,
|
| 1485 |
+
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1486 |
+
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1487 |
+
)
|
| 1488 |
+
|
| 1489 |
+
# Selecionar/Desselecionar todos via checkbox
|
| 1490 |
+
def toggle_selecionar_todos(selecionar, df, coluna_y):
|
| 1491 |
+
"""Marca ou desmarca todas as variáveis independentes."""
|
| 1492 |
+
if selecionar:
|
| 1493 |
+
# Marcar todos
|
| 1494 |
+
colunas_x = [col for col in obter_colunas_numericas(df) if col != coluna_y] if df is not None and coluna_y else []
|
| 1495 |
+
return gr.update(value=colunas_x)
|
| 1496 |
+
else:
|
| 1497 |
+
# Desmarcar todos
|
| 1498 |
+
return gr.update(value=[])
|
| 1499 |
+
|
| 1500 |
+
checkbox_selecionar_todos.change(
|
| 1501 |
+
toggle_selecionar_todos,
|
| 1502 |
+
inputs=[checkbox_selecionar_todos, estado_df, dropdown_y],
|
| 1503 |
+
outputs=[checkboxes_x]
|
| 1504 |
+
)
|
| 1505 |
+
|
| 1506 |
+
# Clique na tabela -> atualiza mapa
|
| 1507 |
+
tabela_dados.select(
|
| 1508 |
+
ao_clicar_tabela,
|
| 1509 |
+
inputs=[estado_df, dropdown_mapa_var],
|
| 1510 |
+
outputs=[mapa_html]
|
| 1511 |
+
)
|
| 1512 |
+
|
| 1513 |
+
# Mudança de variável no dropdown do mapa -> atualiza mapa
|
| 1514 |
+
dropdown_mapa_var.input(
|
| 1515 |
+
atualizar_mapa_callback,
|
| 1516 |
+
inputs=[estado_df_filtrado, estado_df, dropdown_mapa_var],
|
| 1517 |
+
outputs=[mapa_html]
|
| 1518 |
+
)
|
| 1519 |
+
|
| 1520 |
+
# Seleção de X -> atualiza campos de transformação (preview)
|
| 1521 |
+
checkboxes_x.change(
|
| 1522 |
+
_atualizar_campos_transformacoes_com_flag,
|
| 1523 |
+
inputs=[estado_df, checkboxes_x, estado_flag_carregamento, dropdown_y],
|
| 1524 |
+
outputs=[transf_y_row, transf_y_col, transf_y_label] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [estado_flag_carregamento, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1525 |
+
)
|
| 1526 |
+
|
| 1527 |
+
# Mudança de qualquer checkbox de tipo -> atualiza interactive dos dropdowns de transformação
|
| 1528 |
+
_inputs_interativo = [checkboxes_x, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais, estado_df]
|
| 1529 |
+
checkboxes_dicotomicas.change(
|
| 1530 |
+
atualizar_interativo_dicotomicas,
|
| 1531 |
+
inputs=_inputs_interativo,
|
| 1532 |
+
outputs=transf_x_dropdowns
|
| 1533 |
+
)
|
| 1534 |
+
checkboxes_codigo_alocado.change(
|
| 1535 |
+
atualizar_interativo_dicotomicas,
|
| 1536 |
+
inputs=_inputs_interativo,
|
| 1537 |
+
outputs=transf_x_dropdowns
|
| 1538 |
+
)
|
| 1539 |
+
checkboxes_percentuais.change(
|
| 1540 |
+
atualizar_interativo_dicotomicas,
|
| 1541 |
+
inputs=_inputs_interativo,
|
| 1542 |
+
outputs=transf_x_dropdowns
|
| 1543 |
+
)
|
| 1544 |
+
|
| 1545 |
+
# Aplicar seleção de Y -> atualiza X disponíveis e MOSTRA seção 4 (NÃO atualiza estatísticas)
|
| 1546 |
+
btn_aplicar_y.click(
|
| 1547 |
+
lambda df, y: ao_mudar_y_sem_estatisticas(df, y, mostrar_secao_x=True),
|
| 1548 |
+
inputs=[estado_df, dropdown_y],
|
| 1549 |
+
outputs=[checkboxes_x, header_secao_4, accordion_secao_4, estado_flag_carregamento]
|
| 1550 |
+
).then(
|
| 1551 |
+
popular_dicotomicas_callback,
|
| 1552 |
+
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1553 |
+
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1554 |
+
)
|
| 1555 |
+
|
| 1556 |
+
# Aplicar seleção de variáveis X -> atualiza estatísticas, busca transformações, dispersão e micronumerosidade
|
| 1557 |
+
btn_aplicar_selecao_x.click(
|
| 1558 |
+
aplicar_selecao_callback,
|
| 1559 |
+
inputs=[estado_df, dropdown_y, checkboxes_x, estado_outliers_anteriores, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais],
|
| 1560 |
+
outputs=[
|
| 1561 |
+
estado_df_filtrado,
|
| 1562 |
+
tabela_estatisticas,
|
| 1563 |
+
html_micronumerosidade,
|
| 1564 |
+
plot_dispersao,
|
| 1565 |
+
slider_grau_coef, slider_grau_f,
|
| 1566 |
+
busca_html,
|
| 1567 |
+
estado_resultados_busca,
|
| 1568 |
+
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1569 |
+
# Seções 5-9
|
| 1570 |
+
header_secao_5, accordion_secao_5,
|
| 1571 |
+
header_secao_6, accordion_secao_6,
|
| 1572 |
+
header_secao_7, accordion_secao_7,
|
| 1573 |
+
header_secao_8, accordion_secao_8,
|
| 1574 |
+
header_secao_9, accordion_secao_9,
|
| 1575 |
+
] + [transf_y_row, transf_y_col, transf_y_label] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [html_aviso_multicolinearidade]
|
| 1576 |
+
)
|
| 1577 |
+
|
| 1578 |
+
# Re-executar busca ao alterar grau (modo manual, sem fallback)
|
| 1579 |
+
def _rebuscar_transformacoes(df, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, grau_coef, grau_f):
|
| 1580 |
+
if df is None or coluna_y is None or not colunas_x:
|
| 1581 |
+
return (gr.update(), gr.update(), *[gr.update()] * 5)
|
| 1582 |
+
html, resultados, _, *btn_updates = buscar_transformacoes_callback(
|
| 1583 |
+
df, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, int(grau_coef), int(grau_f)
|
| 1584 |
+
)
|
| 1585 |
+
return (html, resultados, *btn_updates)
|
| 1586 |
+
|
| 1587 |
+
_inputs_rebuscar = [estado_df_filtrado, dropdown_y, checkboxes_x, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais, slider_grau_coef, slider_grau_f]
|
| 1588 |
+
_outputs_rebuscar = [busca_html, estado_resultados_busca,
|
| 1589 |
+
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5]
|
| 1590 |
+
|
| 1591 |
+
slider_grau_coef.change(
|
| 1592 |
+
_rebuscar_transformacoes,
|
| 1593 |
+
inputs=_inputs_rebuscar,
|
| 1594 |
+
outputs=_outputs_rebuscar
|
| 1595 |
+
)
|
| 1596 |
+
|
| 1597 |
+
slider_grau_f.change(
|
| 1598 |
+
_rebuscar_transformacoes,
|
| 1599 |
+
inputs=_inputs_rebuscar,
|
| 1600 |
+
outputs=_outputs_rebuscar
|
| 1601 |
+
)
|
| 1602 |
+
|
| 1603 |
+
# Aplicar filtros de outliers
|
| 1604 |
+
btn_aplicar_filtro.click(
|
| 1605 |
+
aplicar_filtros_callback,
|
| 1606 |
+
inputs=[estado_metricas, estado_n_filtros] + filtro_vars + filtro_ops + filtro_vals,
|
| 1607 |
+
outputs=[outliers_texto]
|
| 1608 |
+
)
|
| 1609 |
+
|
| 1610 |
+
# Adicionar filtro
|
| 1611 |
+
btn_adicionar_filtro.click(
|
| 1612 |
+
adicionar_filtro_callback,
|
| 1613 |
+
inputs=[estado_n_filtros],
|
| 1614 |
+
outputs=[estado_n_filtros] + filtro_rows
|
| 1615 |
+
)
|
| 1616 |
+
|
| 1617 |
+
# Remover último filtro
|
| 1618 |
+
btn_remover_ultimo.click(
|
| 1619 |
+
remover_ultimo_filtro_callback,
|
| 1620 |
+
inputs=[estado_n_filtros],
|
| 1621 |
+
outputs=[estado_n_filtros] + filtro_rows
|
| 1622 |
+
)
|
| 1623 |
+
|
| 1624 |
+
# Resetar filtros ao padrão
|
| 1625 |
+
btn_resetar_filtros.click(
|
| 1626 |
+
limpar_filtros_callback,
|
| 1627 |
+
inputs=[],
|
| 1628 |
+
outputs=[estado_n_filtros] + filtro_rows + filtro_vars + filtro_ops + filtro_vals + [outliers_texto]
|
| 1629 |
+
)
|
| 1630 |
+
|
| 1631 |
+
# Atualizar resumo de outliers quando usuário edita os campos
|
| 1632 |
+
outliers_texto.change(
|
| 1633 |
+
atualizar_resumo_outliers,
|
| 1634 |
+
inputs=[estado_outliers_anteriores, outliers_texto, reincluir_texto],
|
| 1635 |
+
outputs=[txt_resumo_outliers]
|
| 1636 |
+
)
|
| 1637 |
+
|
| 1638 |
+
reincluir_texto.change(
|
| 1639 |
+
atualizar_resumo_outliers,
|
| 1640 |
+
inputs=[estado_outliers_anteriores, outliers_texto, reincluir_texto],
|
| 1641 |
+
outputs=[txt_resumo_outliers]
|
| 1642 |
+
)
|
| 1643 |
+
|
| 1644 |
+
# Aplicar filtro também atualiza o resumo
|
| 1645 |
+
btn_aplicar_filtro.click(
|
| 1646 |
+
atualizar_resumo_outliers,
|
| 1647 |
+
inputs=[estado_outliers_anteriores, outliers_texto, reincluir_texto],
|
| 1648 |
+
outputs=[txt_resumo_outliers]
|
| 1649 |
+
)
|
| 1650 |
+
|
| 1651 |
+
# Download da base tratada (CSV)
|
| 1652 |
+
def download_base_callback(df_filtrado, df_original):
|
| 1653 |
+
"""Callback para download da base tratada."""
|
| 1654 |
+
# Usa df_filtrado se disponível, senão usa df_original
|
| 1655 |
+
df_para_exportar = df_filtrado if df_filtrado is not None else df_original
|
| 1656 |
+
|
| 1657 |
+
if df_para_exportar is None:
|
| 1658 |
+
return gr.update(value=None, visible=False)
|
| 1659 |
+
|
| 1660 |
+
try:
|
| 1661 |
+
if hasattr(df_para_exportar, 'empty') and df_para_exportar.empty:
|
| 1662 |
+
return gr.update(value=None, visible=False)
|
| 1663 |
+
|
| 1664 |
+
caminho = exportar_base_csv(df_para_exportar)
|
| 1665 |
+
if caminho:
|
| 1666 |
+
return gr.update(value=caminho, visible=True)
|
| 1667 |
+
else:
|
| 1668 |
+
return gr.update(value=None, visible=False)
|
| 1669 |
+
except Exception as e:
|
| 1670 |
+
print(f"Erro ao exportar CSV: {e}")
|
| 1671 |
+
return gr.update(value=None, visible=False)
|
| 1672 |
+
|
| 1673 |
+
btn_download_base.click(
|
| 1674 |
+
download_base_callback,
|
| 1675 |
+
inputs=[estado_df_filtrado, estado_df],
|
| 1676 |
+
outputs=[download_base_file]
|
| 1677 |
+
)
|
| 1678 |
+
|
| 1679 |
+
# Botões "Adotar Sugestão"
|
| 1680 |
+
btn_adotar_1.click(
|
| 1681 |
+
lambda res, cols_x: adotar_sugestao(0, res, cols_x),
|
| 1682 |
+
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1683 |
+
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1684 |
+
)
|
| 1685 |
+
btn_adotar_2.click(
|
| 1686 |
+
lambda res, cols_x: adotar_sugestao(1, res, cols_x),
|
| 1687 |
+
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1688 |
+
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1689 |
+
)
|
| 1690 |
+
btn_adotar_3.click(
|
| 1691 |
+
lambda res, cols_x: adotar_sugestao(2, res, cols_x),
|
| 1692 |
+
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1693 |
+
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1694 |
+
)
|
| 1695 |
+
btn_adotar_4.click(
|
| 1696 |
+
lambda res, cols_x: adotar_sugestao(3, res, cols_x),
|
| 1697 |
+
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1698 |
+
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1699 |
+
)
|
| 1700 |
+
btn_adotar_5.click(
|
| 1701 |
+
lambda res, cols_x: adotar_sugestao(4, res, cols_x),
|
| 1702 |
+
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1703 |
+
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1704 |
+
)
|
| 1705 |
+
|
| 1706 |
+
# Ajustar modelo (usa dados filtrados se disponíveis) e calcula métricas de outliers
|
| 1707 |
+
dropdown_tipo_grafico_dispersao.change(
|
| 1708 |
+
fn=ao_mudar_tipo_grafico,
|
| 1709 |
+
inputs=[dropdown_tipo_grafico_dispersao, estado_modelo],
|
| 1710 |
+
outputs=[plot_dispersao_transf]
|
| 1711 |
+
)
|
| 1712 |
+
|
| 1713 |
+
btn_ajustar.click(
|
| 1714 |
+
lambda df_filt, df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals: ajustar_modelo_callback(
|
| 1715 |
+
df_filt if df_filt is not None else df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals
|
| 1716 |
+
),
|
| 1717 |
+
inputs=_inputs_ajustar,
|
| 1718 |
+
outputs=_outputs_ajustar
|
| 1719 |
+
).then(
|
| 1720 |
+
popular_campos_avaliacao_callback,
|
| 1721 |
+
inputs=[estado_modelo, tabela_estatisticas],
|
| 1722 |
+
outputs=_outputs_popular_avaliacao
|
| 1723 |
+
)
|
| 1724 |
+
|
| 1725 |
+
# Reiniciar iteração (combina outliers e recomeça) e depois ajusta o modelo
|
| 1726 |
+
btn_reiniciar_iteracao.click(
|
| 1727 |
+
reiniciar_iteracao_callback,
|
| 1728 |
+
inputs=[
|
| 1729 |
+
estado_df, estado_outliers_anteriores, outliers_texto, reincluir_texto,
|
| 1730 |
+
estado_iteracao, dropdown_y, checkboxes_x, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais, slider_grau_coef, slider_grau_f
|
| 1731 |
+
],
|
| 1732 |
+
outputs=[
|
| 1733 |
+
estado_outliers_anteriores,
|
| 1734 |
+
estado_iteracao,
|
| 1735 |
+
estado_df_filtrado,
|
| 1736 |
+
tabela_dados,
|
| 1737 |
+
tabela_estatisticas,
|
| 1738 |
+
header_secao_5,
|
| 1739 |
+
html_outliers_anteriores,
|
| 1740 |
+
html_outliers_sec14,
|
| 1741 |
+
accordion_outliers_anteriores,
|
| 1742 |
+
outliers_texto,
|
| 1743 |
+
reincluir_texto,
|
| 1744 |
+
tabela_metricas,
|
| 1745 |
+
estado_metricas,
|
| 1746 |
+
header_secao_13,
|
| 1747 |
+
txt_resumo_outliers,
|
| 1748 |
+
mapa_html,
|
| 1749 |
+
# Novos outputs para seções 2, 6, 7, 8
|
| 1750 |
+
header_secao_2,
|
| 1751 |
+
html_micronumerosidade,
|
| 1752 |
+
header_secao_6,
|
| 1753 |
+
plot_dispersao,
|
| 1754 |
+
header_secao_7,
|
| 1755 |
+
busca_html,
|
| 1756 |
+
estado_resultados_busca,
|
| 1757 |
+
header_secao_8,
|
| 1758 |
+
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1759 |
+
slider_grau_coef,
|
| 1760 |
+
slider_grau_f,
|
| 1761 |
+
]
|
| 1762 |
+
).then(
|
| 1763 |
+
# Após reiniciar, ajusta modelo automaticamente para calcular métricas de outliers
|
| 1764 |
+
lambda df_filt, df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals: ajustar_modelo_callback(
|
| 1765 |
+
df_filt if df_filt is not None else df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals
|
| 1766 |
+
),
|
| 1767 |
+
inputs=_inputs_ajustar,
|
| 1768 |
+
outputs=_outputs_ajustar
|
| 1769 |
+
).then(
|
| 1770 |
+
popular_campos_avaliacao_callback,
|
| 1771 |
+
inputs=[estado_modelo, tabela_estatisticas],
|
| 1772 |
+
outputs=_outputs_popular_avaliacao
|
| 1773 |
+
).then(
|
| 1774 |
+
fn=None,
|
| 1775 |
+
inputs=None,
|
| 1776 |
+
outputs=None,
|
| 1777 |
+
js="""() => {
|
| 1778 |
+
if ('parentIFrame' in window) {
|
| 1779 |
+
window.parentIFrame.scrollTo({top: 0, behavior: 'smooth'});
|
| 1780 |
+
} else {
|
| 1781 |
+
window.scrollTo({top: 0, behavior: 'smooth'});
|
| 1782 |
+
}
|
| 1783 |
+
}"""
|
| 1784 |
+
)
|
| 1785 |
+
|
| 1786 |
+
# Limpar histórico de outliers
|
| 1787 |
+
btn_limpar_historico.click(
|
| 1788 |
+
limpar_historico_callback,
|
| 1789 |
+
inputs=[estado_df],
|
| 1790 |
+
outputs=[
|
| 1791 |
+
estado_outliers_anteriores,
|
| 1792 |
+
estado_iteracao,
|
| 1793 |
+
estado_df_filtrado,
|
| 1794 |
+
tabela_dados,
|
| 1795 |
+
tabela_estatisticas,
|
| 1796 |
+
mapa_html,
|
| 1797 |
+
html_outliers_anteriores,
|
| 1798 |
+
accordion_outliers_anteriores,
|
| 1799 |
+
header_secao_2,
|
| 1800 |
+
# Reset seções 4-16 (states)
|
| 1801 |
+
estado_modelo, estado_metricas, estado_resultados_busca, estado_avaliacoes,
|
| 1802 |
+
# Headers, accordions e conteúdo das seções 4-16
|
| 1803 |
+
header_secao_4, accordion_secao_4, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais,
|
| 1804 |
+
html_aviso_multicolinearidade,
|
| 1805 |
+
header_secao_5, accordion_secao_5,
|
| 1806 |
+
header_secao_6, accordion_secao_6, html_micronumerosidade,
|
| 1807 |
+
header_secao_7, accordion_secao_7, plot_dispersao,
|
| 1808 |
+
header_secao_8, accordion_secao_8, slider_grau_coef, slider_grau_f, busca_html,
|
| 1809 |
+
header_secao_9, accordion_secao_9, transformacao_y,
|
| 1810 |
+
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1811 |
+
] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [
|
| 1812 |
+
header_secao_10, accordion_secao_10, dropdown_tipo_grafico_dispersao, plot_dispersao_transf,
|
| 1813 |
+
header_secao_11, accordion_secao_11, diagnosticos_html, tabela_coef, tabela_obs_calc,
|
| 1814 |
+
header_secao_12, accordion_secao_12, plot_obs_calc, plot_residuos, plot_hist, plot_cook, plot_corr,
|
| 1815 |
+
header_secao_13, accordion_secao_13, tabela_metricas,
|
| 1816 |
+
header_secao_14, accordion_secao_14, html_outliers_sec14, outliers_texto, reincluir_texto, txt_resumo_outliers,
|
| 1817 |
+
# Seção 15: Avaliação de Imóvel
|
| 1818 |
+
header_secao_15, accordion_secao_15,
|
| 1819 |
+
] + aval_rows + aval_inputs + [
|
| 1820 |
+
resultado_avaliacao_html, dropdown_base_avaliacao, excluir_aval_trigger, download_avaliacoes_file,
|
| 1821 |
+
# Seção 16: Exportar Modelo
|
| 1822 |
+
header_secao_16, accordion_secao_16, nome_arquivo, status_exportar,
|
| 1823 |
+
] + filtro_vars
|
| 1824 |
+
)
|
| 1825 |
+
|
| 1826 |
+
# Exportar (usa dados filtrados se disponíveis)
|
| 1827 |
+
btn_exportar.click(
|
| 1828 |
+
lambda res_modelo, df_filt, df_orig, stats, nome, elab_nome, outliers: exportar_modelo_callback(
|
| 1829 |
+
res_modelo, df_filt if df_filt is not None else df_orig, df_orig, stats, nome,
|
| 1830 |
+
_avaliadores_dict.get(elab_nome) if elab_nome else None,
|
| 1831 |
+
outliers or []
|
| 1832 |
+
),
|
| 1833 |
+
inputs=[estado_modelo, estado_df_filtrado, estado_df, tabela_estatisticas, nome_arquivo, dropdown_elaborador, estado_outliers_anteriores],
|
| 1834 |
+
outputs=[status_exportar, download_modelo_file]
|
| 1835 |
+
)
|
| 1836 |
+
|
| 1837 |
+
# Avaliação de imóvel (seção 15)
|
| 1838 |
+
btn_calcular_avaliacao.click(
|
| 1839 |
+
avaliar_imovel_callback,
|
| 1840 |
+
inputs=[estado_modelo, tabela_estatisticas, estado_avaliacoes, dropdown_base_avaliacao] + aval_inputs,
|
| 1841 |
+
outputs=[resultado_avaliacao_html, estado_avaliacoes, dropdown_base_avaliacao]
|
| 1842 |
+
)
|
| 1843 |
+
|
| 1844 |
+
btn_limpar_avaliacoes.click(
|
| 1845 |
+
limpar_avaliacoes_callback,
|
| 1846 |
+
inputs=[],
|
| 1847 |
+
outputs=[resultado_avaliacao_html, estado_avaliacoes, dropdown_base_avaliacao]
|
| 1848 |
+
)
|
| 1849 |
+
|
| 1850 |
+
excluir_aval_trigger.change(
|
| 1851 |
+
excluir_avaliacao_callback,
|
| 1852 |
+
inputs=[excluir_aval_trigger, estado_avaliacoes, dropdown_base_avaliacao],
|
| 1853 |
+
outputs=[resultado_avaliacao_html, estado_avaliacoes, dropdown_base_avaliacao, excluir_aval_trigger]
|
| 1854 |
+
)
|
| 1855 |
+
|
| 1856 |
+
dropdown_base_avaliacao.change(
|
| 1857 |
+
atualizar_base_avaliacao_callback,
|
| 1858 |
+
inputs=[estado_avaliacoes, dropdown_base_avaliacao],
|
| 1859 |
+
outputs=[resultado_avaliacao_html]
|
| 1860 |
+
)
|
| 1861 |
+
|
| 1862 |
+
btn_exportar_avaliacoes.click(
|
| 1863 |
+
exportar_avaliacoes_excel_callback,
|
| 1864 |
+
inputs=[estado_avaliacoes],
|
| 1865 |
+
outputs=[download_avaliacoes_file]
|
| 1866 |
+
)
|
| 1867 |
+
|
| 1868 |
+
# Downloads de tabelas
|
| 1869 |
+
btn_download_estatisticas.click(
|
| 1870 |
+
lambda df: download_tabela_callback(df, "estatisticas"),
|
| 1871 |
+
inputs=[tabela_estatisticas],
|
| 1872 |
+
outputs=[download_estatisticas_file]
|
| 1873 |
+
)
|
| 1874 |
+
|
| 1875 |
+
btn_download_dados.click(
|
| 1876 |
+
lambda df: download_tabela_callback(df, "dados"),
|
| 1877 |
+
inputs=[tabela_dados],
|
| 1878 |
+
outputs=[download_dados_file]
|
| 1879 |
+
)
|
| 1880 |
+
|
| 1881 |
+
btn_download_coef.click(
|
| 1882 |
+
lambda df: download_tabela_callback(df, "coeficientes"),
|
| 1883 |
+
inputs=[tabela_coef],
|
| 1884 |
+
outputs=[download_coef_file]
|
| 1885 |
+
)
|
| 1886 |
+
|
| 1887 |
+
btn_download_obs_calc.click(
|
| 1888 |
+
lambda df: download_tabela_callback(df, "obs_calc"),
|
| 1889 |
+
inputs=[tabela_obs_calc],
|
| 1890 |
+
outputs=[download_obs_calc_file]
|
| 1891 |
+
)
|
| 1892 |
+
|
| 1893 |
+
btn_download_metricas.click(
|
| 1894 |
+
lambda df: download_tabela_callback(df, "metricas"),
|
| 1895 |
+
inputs=[tabela_metricas],
|
| 1896 |
+
outputs=[download_metricas_file]
|
| 1897 |
+
)
|
| 1898 |
+
|
| 1899 |
+
|
| 1900 |
+
|
| 1901 |
+
# ============================================================
|
| 1902 |
+
# MAIN
|
| 1903 |
+
# ============================================================
|
| 1904 |
+
|
| 1905 |
+
if __name__ == "__main__":
|
| 1906 |
+
css = carregar_css()
|
| 1907 |
+
with gr.Blocks(title="Elaboração de Modelos", css=css) as app:
|
| 1908 |
+
gr.Markdown(TITULO)
|
| 1909 |
+
criar_aba()
|
| 1910 |
+
app.queue().launch()
|
backend/app/core/elaboracao/avaliadores.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"avaliadores": [
|
| 3 |
+
{
|
| 4 |
+
"nome_completo": "David Schuch Bertoglio",
|
| 5 |
+
"titulo": "Coordenador da Equipe de Suporte, Judiciais e Locações",
|
| 6 |
+
"cargo": "Engenheiro",
|
| 7 |
+
"conselho": "CREA",
|
| 8 |
+
"numero_conselho": "114.44",
|
| 9 |
+
"estado_conselho": "RS",
|
| 10 |
+
"matricula_sem_digito": "95654801",
|
| 11 |
+
"lotacao": "SMF/RM/DAI/ESJL"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"nome_completo": "Jéssica Lange",
|
| 15 |
+
"titulo": "Auxiliar da Equipe de Avaliações",
|
| 16 |
+
"cargo": "Arquiteta",
|
| 17 |
+
"conselho": "CAU",
|
| 18 |
+
"numero_conselho": "A64935-0",
|
| 19 |
+
"estado_conselho": "RS",
|
| 20 |
+
"matricula_sem_digito": "1279688",
|
| 21 |
+
"lotacao": "EAV/DAI/RM/SMF"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"nome_completo": "Debora Fonseca Alves",
|
| 25 |
+
"titulo": "Auxiliar da Equipe de Avaliações",
|
| 26 |
+
"cargo": "Engenheira",
|
| 27 |
+
"conselho": "CREA",
|
| 28 |
+
"numero_conselho": "190.257",
|
| 29 |
+
"estado_conselho": "RS",
|
| 30 |
+
"matricula_sem_digito": "1507850",
|
| 31 |
+
"lotacao": "SMF/RM/DAI/EAV"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"nome_completo": "Vanessa Staats Basso",
|
| 35 |
+
"titulo": "Auxiliar da Equipe de Avaliações",
|
| 36 |
+
"cargo": "Engenheira",
|
| 37 |
+
"conselho": "CREA",
|
| 38 |
+
"numero_conselho": "194.957",
|
| 39 |
+
"estado_conselho": "RS",
|
| 40 |
+
"matricula_sem_digito": "1507540",
|
| 41 |
+
"lotacao": "SMF/RM/DAI/EAV"
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"nome_completo": "Fernanda Pontel",
|
| 45 |
+
"titulo": "Auxiliar da Equipe de Suporte, Judiciais e Locações",
|
| 46 |
+
"cargo": "Arquiteta",
|
| 47 |
+
"conselho": "CAU",
|
| 48 |
+
"numero_conselho": "A50731-8",
|
| 49 |
+
"estado_conselho": "RS",
|
| 50 |
+
"matricula_sem_digito": "1036130",
|
| 51 |
+
"lotacao": "SMF/RM/DAI/ESJL"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"nome_completo": "Adriana Kirsch Bissigo",
|
| 55 |
+
"titulo": "Auxiliar da Equipe de Suporte, Judiciais e Locações",
|
| 56 |
+
"cargo": "Engenheira",
|
| 57 |
+
"conselho": "CREA_RS",
|
| 58 |
+
"numero_conselho": "69342",
|
| 59 |
+
"estado_conselho": "RS",
|
| 60 |
+
"matricula_sem_digito": "188661",
|
| 61 |
+
"lotacao": "SMF/RM/DAI/ESJL"
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"nome_completo": "Roberta Brenner Ayub",
|
| 65 |
+
"titulo": "Arquiteta da Equipe de Suporte, Judiciais e Locações",
|
| 66 |
+
"cargo": "Arquiteta",
|
| 67 |
+
"conselho": "CAU",
|
| 68 |
+
"numero_conselho": "A29665-1",
|
| 69 |
+
"estado_conselho": "RS",
|
| 70 |
+
"matricula_sem_digito": "370440",
|
| 71 |
+
"lotacao": "SMF/RM/DAI/ESJL"
|
| 72 |
+
}
|
| 73 |
+
]
|
| 74 |
+
}
|
backend/app/core/elaboracao/carregamento.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
carregamento.py - Carga de arquivos, reset de estado e inicialização.
|
| 4 |
+
|
| 5 |
+
Callbacks que lidam com upload de CSV/Excel/.dai,
|
| 6 |
+
seleção de aba Excel, reset de seções e limpar histórico.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
import os
|
| 11 |
+
from datetime import datetime, timezone, timedelta
|
| 12 |
+
|
| 13 |
+
from .formatadores import arredondar_df, criar_header_secao, formatar_lista_variaveis_html
|
| 14 |
+
from .modelo import (
|
| 15 |
+
aplicar_selecao_callback,
|
| 16 |
+
ajustar_modelo_callback,
|
| 17 |
+
MAX_VARS_X,
|
| 18 |
+
)
|
| 19 |
+
from .core import (
|
| 20 |
+
detectar_abas_excel,
|
| 21 |
+
carregar_arquivo,
|
| 22 |
+
carregar_dai,
|
| 23 |
+
obter_colunas_numericas,
|
| 24 |
+
identificar_coluna_y_padrao,
|
| 25 |
+
formatar_transformacao,
|
| 26 |
+
)
|
| 27 |
+
from .charts import criar_mapa
|
| 28 |
+
from .geocodificacao import verificar_coords, auto_detectar_colunas_geo, padronizar_coords
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ============================================================
|
| 32 |
+
# RESET DE SEÇÕES
|
| 33 |
+
# ============================================================
|
| 34 |
+
|
| 35 |
+
def _valores_reset_secoes_4_a_16():
|
| 36 |
+
"""Valores de reset para seções 4-16 (headers, accordions e conteúdo).
|
| 37 |
+
|
| 38 |
+
Headers são resetados para remover timestamps. Accordions são fechados.
|
| 39 |
+
Usado por carregar_dados_do_arquivo e limpar_historico_callback para limpar
|
| 40 |
+
todas as seções quando um novo arquivo é carregado ou o histórico é resetado.
|
| 41 |
+
|
| 42 |
+
CONTRACT: Retorna 156 itens.
|
| 43 |
+
Se alterar, atualizar: TODAS as funções neste arquivo que retornam 193 itens
|
| 44 |
+
(ao_carregar_arquivo, _resultado_selecao_aba, confirmar_aba_callback,
|
| 45 |
+
carregar_dados_de_dai, carregar_dados_do_arquivo) + limpar_historico_callback.
|
| 46 |
+
"""
|
| 47 |
+
n_rows = (MAX_VARS_X + 7) // 8 # 3
|
| 48 |
+
return (
|
| 49 |
+
# States
|
| 50 |
+
None, # estado_modelo
|
| 51 |
+
None, # estado_metricas
|
| 52 |
+
[], # estado_resultados_busca
|
| 53 |
+
[], # estado_avaliacoes
|
| 54 |
+
# Section 4
|
| 55 |
+
gr.update(value=criar_header_secao(4, "Selecionar Variáveis Independentes")), # header_secao_4
|
| 56 |
+
gr.update(visible=False), # accordion_secao_4
|
| 57 |
+
gr.update(choices=[], value=[], visible=False), # checkboxes_dicotomicas
|
| 58 |
+
gr.update(choices=[], value=[], visible=False), # checkboxes_codigo_alocado
|
| 59 |
+
gr.update(choices=[], value=[], visible=False), # checkboxes_percentuais
|
| 60 |
+
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 61 |
+
# Section 5
|
| 62 |
+
gr.update(value=criar_header_secao(5, "Estatísticas das Variáveis Selecionadas")), # header_secao_5
|
| 63 |
+
gr.update(visible=False), # accordion_secao_5
|
| 64 |
+
# Section 6
|
| 65 |
+
gr.update(value=criar_header_secao(6, "Teste de Micronumerosidade (NBR 14.653-2)")), # header_secao_6
|
| 66 |
+
gr.update(visible=False), # accordion_secao_6
|
| 67 |
+
"", # html_micronumerosidade
|
| 68 |
+
# Section 7
|
| 69 |
+
gr.update(value=criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes")), # header_secao_7
|
| 70 |
+
gr.update(visible=False), # accordion_secao_7
|
| 71 |
+
None, # plot_dispersao
|
| 72 |
+
# Section 8
|
| 73 |
+
gr.update(value=criar_header_secao(8, "Transformações Sugeridas")), # header_secao_8
|
| 74 |
+
gr.update(visible=False), # accordion_secao_8
|
| 75 |
+
gr.update(value=3), # slider_grau_coef
|
| 76 |
+
gr.update(value=3), # slider_grau_f
|
| 77 |
+
"", # busca_html
|
| 78 |
+
# Section 9
|
| 79 |
+
gr.update(value=criar_header_secao(9, "Aplicação das Transformações")), # header_secao_9
|
| 80 |
+
gr.update(visible=False), # accordion_secao_9
|
| 81 |
+
gr.update(value="(x)"), # transformacao_y
|
| 82 |
+
*[gr.update(visible=False) for _ in range(5)], # btn_adotar 1-5
|
| 83 |
+
gr.update(visible=False), # transf_y_row
|
| 84 |
+
gr.update(visible=False), # transf_y_col
|
| 85 |
+
gr.update(value="", visible=False), # transf_y_label
|
| 86 |
+
# transf_x_rows, transf_x_columns, transf_x_labels, transf_x_dropdowns (63 itens)
|
| 87 |
+
*[gr.update(visible=False) for _ in range(n_rows)],
|
| 88 |
+
*[gr.update(visible=False) for _ in range(MAX_VARS_X)],
|
| 89 |
+
*[gr.update(value="", visible=False) for _ in range(MAX_VARS_X)],
|
| 90 |
+
*[gr.update(value="(x)", interactive=True, visible=False) for _ in range(MAX_VARS_X)],
|
| 91 |
+
# Section 10
|
| 92 |
+
gr.update(value=criar_header_secao(10, "Gráficos de Dispersão (Variáveis Transformadas)")), # header_secao_10
|
| 93 |
+
gr.update(visible=False), # accordion_secao_10
|
| 94 |
+
gr.update(value="Variáveis Independentes Transformadas X Variável Dependente Transformada", visible=False), # dropdown_tipo_grafico_dispersao
|
| 95 |
+
None, # plot_dispersao_transf
|
| 96 |
+
# Section 11
|
| 97 |
+
gr.update(value=criar_header_secao(11, "Diagnóstico de Modelo")), # header_secao_11
|
| 98 |
+
gr.update(visible=False), # accordion_secao_11
|
| 99 |
+
"", # diagnosticos_html
|
| 100 |
+
None, # tabela_coef
|
| 101 |
+
None, # tabela_obs_calc
|
| 102 |
+
# Section 12
|
| 103 |
+
gr.update(value=criar_header_secao(12, "Gráficos de Diagnóstico do Modelo")), # header_secao_12
|
| 104 |
+
gr.update(visible=False), # accordion_secao_12
|
| 105 |
+
None, None, None, None, None, # plots: obs_calc, residuos, hist, cook, corr
|
| 106 |
+
# Section 13
|
| 107 |
+
gr.update(value=criar_header_secao(13, "Analisar Outliers")), # header_secao_13
|
| 108 |
+
gr.update(visible=False), # accordion_secao_13
|
| 109 |
+
None, # tabela_metricas
|
| 110 |
+
# Section 14 (exclusão)
|
| 111 |
+
gr.update(value=criar_header_secao(14, "Exclusão ou Reinclusão de Outliers")), # header_secao_14
|
| 112 |
+
gr.update(visible=False), # accordion_secao_14
|
| 113 |
+
"", # html_outliers_sec14
|
| 114 |
+
"", # outliers_texto
|
| 115 |
+
"", # reincluir_texto
|
| 116 |
+
"Excluídos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0", # txt_resumo_outliers
|
| 117 |
+
# Section 15 (Avaliação de Imóvel)
|
| 118 |
+
gr.update(value=criar_header_secao(15, "Avaliação de Imóvel")), # header_secao_15
|
| 119 |
+
gr.update(visible=False), # accordion_secao_15
|
| 120 |
+
*[gr.update(visible=False) for _ in range(MAX_VARS_X // 4)], # aval_rows (5)
|
| 121 |
+
*[gr.update(visible=False, value=None, label="") for _ in range(MAX_VARS_X)], # aval_inputs (20)
|
| 122 |
+
"", # resultado_avaliacao_html
|
| 123 |
+
gr.update(choices=[], value=None), # dropdown_base_avaliacao
|
| 124 |
+
"", # excluir_aval_trigger
|
| 125 |
+
gr.update(value=None, visible=False), # download_avaliacoes_file
|
| 126 |
+
# Section 16 (Exportar Modelo)
|
| 127 |
+
gr.update(value=criar_header_secao(16, "Exportar Modelo")), # header_secao_16
|
| 128 |
+
gr.update(visible=False), # accordion_secao_16
|
| 129 |
+
gr.update(value=""), # nome_arquivo
|
| 130 |
+
gr.update(value=""), # status_exportar
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ============================================================
|
| 135 |
+
# CALLBACKS DE CARREGAMENTO
|
| 136 |
+
# ============================================================
|
| 137 |
+
|
| 138 |
+
def ao_carregar_arquivo(arquivo):
|
| 139 |
+
"""Callback quando arquivo é carregado. Detecta se há múltiplas abas no Excel.
|
| 140 |
+
|
| 141 |
+
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 142 |
+
Se alterar, atualizar: _outputs_carregar em app.py + TODAS as 5 funções neste arquivo.
|
| 143 |
+
"""
|
| 144 |
+
caminho_arquivo = arquivo.name if hasattr(arquivo, 'name') else str(arquivo)
|
| 145 |
+
nome_exibicao = "Arquivo carregado: " + os.path.basename(caminho_arquivo)
|
| 146 |
+
|
| 147 |
+
# Se for arquivo .dai, carrega modelo completo
|
| 148 |
+
if caminho_arquivo.endswith('.dai'):
|
| 149 |
+
return carregar_dados_de_dai(caminho_arquivo)
|
| 150 |
+
|
| 151 |
+
# Detecta abas do Excel
|
| 152 |
+
abas, msg_abas, sucesso_abas = detectar_abas_excel(caminho_arquivo)
|
| 153 |
+
|
| 154 |
+
# Se há múltiplas abas, mostra seletor de aba (não carrega dados ainda)
|
| 155 |
+
if sucesso_abas and len(abas) > 1:
|
| 156 |
+
return _resultado_selecao_aba(caminho_arquivo, abas)
|
| 157 |
+
|
| 158 |
+
# Se não há múltiplas abas, carrega diretamente
|
| 159 |
+
return carregar_dados_do_arquivo(caminho_arquivo, None,
|
| 160 |
+
nome_arquivo_exibicao=nome_exibicao)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _resultado_selecao_aba(caminho_arquivo, abas):
|
| 164 |
+
"""Retorna tupla para estado B: mostra seletor de aba, não carrega dados ainda.
|
| 165 |
+
|
| 166 |
+
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 167 |
+
"""
|
| 168 |
+
return (
|
| 169 |
+
None, # estado_df (sem dados ainda)
|
| 170 |
+
"Arquivo com múltiplas abas detectado. Selecione uma aba e confirme.", # status
|
| 171 |
+
gr.update(choices=abas, value=abas[0]), # dropdown_aba
|
| 172 |
+
gr.update(choices=[], value=None), # dropdown_y
|
| 173 |
+
None, # tabela_dados
|
| 174 |
+
None, # tabela_estatisticas
|
| 175 |
+
gr.update(choices=[]), # checkboxes_x
|
| 176 |
+
"<p>Carregue um arquivo para ver o mapa.</p>", # mapa
|
| 177 |
+
None, # estado_df_filtrado
|
| 178 |
+
[], # estado_outliers_anteriores
|
| 179 |
+
1, # estado_iteracao
|
| 180 |
+
gr.update(visible=False), # accordion_outliers_anteriores
|
| 181 |
+
"", # html_outliers_anteriores
|
| 182 |
+
caminho_arquivo, # estado_arquivo_temp (manter caminho para confirmar_aba)
|
| 183 |
+
# Seções 2 e 3 - ocultas
|
| 184 |
+
gr.update(visible=False), # header_secao_2
|
| 185 |
+
gr.update(visible=False), # accordion_secao_2
|
| 186 |
+
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"), # dropdown_mapa_var
|
| 187 |
+
gr.update(visible=False), # header_secao_3
|
| 188 |
+
gr.update(visible=False), # accordion_secao_3
|
| 189 |
+
# Reset seções 4-16
|
| 190 |
+
*_valores_reset_secoes_4_a_16(),
|
| 191 |
+
# Controles de visibilidade da seção 1
|
| 192 |
+
gr.update(visible=False), # row_upload (ocultar upload)
|
| 193 |
+
gr.update(visible=True), # row_selecao_aba (mostrar seletor de aba)
|
| 194 |
+
gr.update(visible=False), # row_pos_carga (ocultar restart)
|
| 195 |
+
gr.update(value=""), # html_nome_arquivo_carregado
|
| 196 |
+
False, # estado_flag_carregamento
|
| 197 |
+
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 198 |
+
# Painel de coordenadas (no-op — dados ainda não carregados)
|
| 199 |
+
gr.update(visible=False), gr.update(value=""),
|
| 200 |
+
gr.update(choices=[]), gr.update(choices=[]),
|
| 201 |
+
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 202 |
+
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 203 |
+
gr.update(visible=False), # row_opcao_mapear
|
| 204 |
+
gr.update(visible=False), # row_opcao_geocodificar
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def confirmar_aba_callback(caminho_arquivo, nome_aba):
|
| 209 |
+
"""Callback quando usuário confirma seleção de aba para Excel multi-aba.
|
| 210 |
+
|
| 211 |
+
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 212 |
+
"""
|
| 213 |
+
if caminho_arquivo is None or nome_aba is None:
|
| 214 |
+
return (
|
| 215 |
+
None, "Erro: arquivo não encontrado. Recarregue a página.",
|
| 216 |
+
gr.update(choices=[], value=None), # dropdown_aba
|
| 217 |
+
gr.update(choices=[], value=None), # dropdown_y
|
| 218 |
+
None, None, gr.update(choices=[]),
|
| 219 |
+
"<p>Carregue um arquivo para ver o mapa.</p>",
|
| 220 |
+
None, [], 1, gr.update(visible=False),
|
| 221 |
+
"", None,
|
| 222 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 223 |
+
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"),
|
| 224 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 225 |
+
*_valores_reset_secoes_4_a_16(),
|
| 226 |
+
# Volta ao estado de upload em caso de erro
|
| 227 |
+
gr.update(visible=True), # row_upload
|
| 228 |
+
gr.update(visible=False), # row_selecao_aba
|
| 229 |
+
gr.update(visible=False), # row_pos_carga
|
| 230 |
+
gr.update(value=""), # html_nome_arquivo_carregado
|
| 231 |
+
False, # estado_flag_carregamento
|
| 232 |
+
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 233 |
+
# Painel de coordenadas (no-op — erro)
|
| 234 |
+
gr.update(visible=False), gr.update(value=""),
|
| 235 |
+
gr.update(choices=[]), gr.update(choices=[]),
|
| 236 |
+
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 237 |
+
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 238 |
+
gr.update(visible=False), # row_opcao_mapear
|
| 239 |
+
gr.update(visible=False), # row_opcao_geocodificar
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
nome_exibicao = "Arquivo carregado: "+os.path.basename(caminho_arquivo)
|
| 243 |
+
nome_exibicao_completo = f"{nome_exibicao} — Aba: {nome_aba}"
|
| 244 |
+
return carregar_dados_do_arquivo(caminho_arquivo, nome_aba,
|
| 245 |
+
nome_arquivo_exibicao=nome_exibicao_completo)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def carregar_dados_de_dai(caminho_arquivo):
|
| 249 |
+
"""Carrega arquivo .dai e popula toda a interface com o modelo reconstruído.
|
| 250 |
+
|
| 251 |
+
Executa o mesmo fluxo que o usuário faria manualmente:
|
| 252 |
+
1. Carrega dados (como carregar_dados_do_arquivo)
|
| 253 |
+
2. Aplica seleção de variáveis (como aplicar_selecao_callback)
|
| 254 |
+
3. Sobrescreve transformações com valores do .dai
|
| 255 |
+
4. Ajusta modelo (como ajustar_modelo_callback)
|
| 256 |
+
|
| 257 |
+
CONTRACT: Retorna 196 itens para _outputs_carregar (app.py:criar_aba).
|
| 258 |
+
Consome aplicar_selecao_callback por índice (r[0], r[1], ..., r[23:]).
|
| 259 |
+
Consome ajustar_modelo_callback por índice (m[0], ..., m[32]).
|
| 260 |
+
"""
|
| 261 |
+
df, coluna_y, colunas_x, transformacao_y, transformacoes_x, dicotomicas, codigo_alocado, percentuais, msg, sucesso, elaborador, outliers_excluidos = carregar_dai(caminho_arquivo)
|
| 262 |
+
|
| 263 |
+
nome_exibicao = os.path.basename(caminho_arquivo)
|
| 264 |
+
html_nome = f"<h2 style='margin:0 0 12px 0; font-size:1.4em;'>{nome_exibicao}</h2>"
|
| 265 |
+
if elaborador:
|
| 266 |
+
nome_elab = elaborador.get("nome_completo", "")
|
| 267 |
+
cargo = elaborador.get("cargo", "")
|
| 268 |
+
conselho = elaborador.get("conselho", "")
|
| 269 |
+
num_conselho = elaborador.get("numero_conselho", "")
|
| 270 |
+
estado_conselho = elaborador.get("estado_conselho", "")
|
| 271 |
+
matricula = elaborador.get("matricula_sem_digito", "")
|
| 272 |
+
lotacao = elaborador.get("lotacao", "")
|
| 273 |
+
linha2 = f"{cargo} · {conselho}/{estado_conselho} {num_conselho}" if cargo else ""
|
| 274 |
+
linha3 = f"Matrícula: {matricula} · {lotacao}" if matricula else ""
|
| 275 |
+
if nome_elab:
|
| 276 |
+
info_variaveis = (
|
| 277 |
+
[f"{coluna_y}: {formatar_transformacao(transformacao_y, is_y=True)}"]
|
| 278 |
+
+ [f"{col}: {formatar_transformacao(transformacoes_x.get(col, '(x)'))}"
|
| 279 |
+
for col in colunas_x]
|
| 280 |
+
)
|
| 281 |
+
variaveis_lado_direito = formatar_lista_variaveis_html(info_variaveis)
|
| 282 |
+
html_nome += (
|
| 283 |
+
'<div style="display:flex; justify-content:space-between; align-items:flex-start; '
|
| 284 |
+
'gap:24px; background:#e9ecef; border-left:5px solid #6c757d; '
|
| 285 |
+
'border-radius:6px; padding:14px 18px; color:#495057; line-height:1.8; margin-top:8px;">'
|
| 286 |
+
'<div>'
|
| 287 |
+
f'<span style="display:block; font-size:1.15em; font-weight:600; color:#212529; margin-bottom:4px;">{nome_elab}</span>'
|
| 288 |
+
+ (f'<span style="display:block; font-size:1em;">{linha2}</span>' if linha2 else '')
|
| 289 |
+
+ (f'<span style="display:block; font-size:0.95em; color:#6c757d;">{linha3}</span>' if linha3 else '')
|
| 290 |
+
+ '</div>'
|
| 291 |
+
+ f'<div>{variaveis_lado_direito}</div>'
|
| 292 |
+
+ '</div>'
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
if outliers_excluidos:
|
| 296 |
+
lista_str = ", ".join(map(str, sorted(outliers_excluidos)))
|
| 297 |
+
n = len(outliers_excluidos)
|
| 298 |
+
html_outliers_content = (
|
| 299 |
+
'<div style="display:flex; gap:16px; align-items:baseline; padding:10px 16px; '
|
| 300 |
+
'background:var(--background-fill-secondary,#f8f9fa); border-radius:8px; '
|
| 301 |
+
'border:1px solid var(--border-color-primary,#e2e8f0); flex-wrap:wrap;">'
|
| 302 |
+
f'<span style="font-weight:600; color:var(--body-text-color,#495057); white-space:nowrap;">'
|
| 303 |
+
f'{n} outlier(s) excluídos do modelo ajustado</span>'
|
| 304 |
+
f'<span style="color:var(--body-text-color-subdued,#6c757d); font-size:0.92em;">'
|
| 305 |
+
f'Índices: {lista_str}</span>'
|
| 306 |
+
'</div>'
|
| 307 |
+
)
|
| 308 |
+
else:
|
| 309 |
+
html_outliers_content = ""
|
| 310 |
+
|
| 311 |
+
if not sucesso:
|
| 312 |
+
return (
|
| 313 |
+
None, msg, gr.update(), gr.update(choices=[], value=None),
|
| 314 |
+
None, None, gr.update(choices=[]), "<p>Erro ao carregar modelo.</p>",
|
| 315 |
+
None, [], 1, gr.update(visible=False),
|
| 316 |
+
"", None,
|
| 317 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 318 |
+
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"),
|
| 319 |
+
gr.update(visible=False), gr.update(visible=False),
|
| 320 |
+
*_valores_reset_secoes_4_a_16(),
|
| 321 |
+
# Volta ao estado de upload em caso de erro
|
| 322 |
+
gr.update(visible=True), # row_upload
|
| 323 |
+
gr.update(visible=False), # row_selecao_aba
|
| 324 |
+
gr.update(visible=False), # row_pos_carga
|
| 325 |
+
gr.update(value=""), # html_nome_arquivo_carregado
|
| 326 |
+
False, # estado_flag_carregamento
|
| 327 |
+
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 328 |
+
# Painel de coordenadas (no-op — .dai isento)
|
| 329 |
+
gr.update(visible=False), gr.update(value=""),
|
| 330 |
+
gr.update(choices=[]), gr.update(choices=[]),
|
| 331 |
+
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 332 |
+
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 333 |
+
gr.update(visible=False), # row_opcao_mapear
|
| 334 |
+
gr.update(visible=False), # row_opcao_geocodificar
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
colunas_numericas = obter_colunas_numericas(df)
|
| 338 |
+
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 339 |
+
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 340 |
+
mapa_html_val = criar_mapa(df)
|
| 341 |
+
|
| 342 |
+
# --- Passo 2: Simula "Aplicar Seleção" (seções 5-9) ---
|
| 343 |
+
r = aplicar_selecao_callback(df, coluna_y, colunas_x, outliers_excluidos, dicotomicas, codigo_alocado, percentuais)
|
| 344 |
+
# r: [df_filtrado, tabela_est, html_micro, plot_disp, slider_coef, slider_f,
|
| 345 |
+
# busca_html, resultados, btn1-5(5), secoes5-9 headers/accordions(10), campos_transf(66)]
|
| 346 |
+
|
| 347 |
+
# --- Passo 3: Sobrescreve dropdowns de transformação com valores do .dai ---
|
| 348 |
+
campos_transf = list(r[23:89]) # 66 itens: y_row(1)+y_col(1)+y_label(1)+rows(3)+columns(20)+labels(20)+dropdowns(20)
|
| 349 |
+
n_rows = (MAX_VARS_X + 7) // 8
|
| 350 |
+
dropdown_offset = 3 + n_rows + MAX_VARS_X + MAX_VARS_X # 3 + 3 + 20 + 20 = 46
|
| 351 |
+
for i, col in enumerate(colunas_x):
|
| 352 |
+
if i < MAX_VARS_X:
|
| 353 |
+
# Dicotômicas e percentuais: travar. Código alocado: livre.
|
| 354 |
+
travar = col in dicotomicas or col in percentuais
|
| 355 |
+
campos_transf[dropdown_offset + i] = gr.update(
|
| 356 |
+
value=transformacoes_x.get(col, "(x)"),
|
| 357 |
+
interactive=not travar,
|
| 358 |
+
visible=True
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
# --- Passo 4: Simula "Ajustar Modelo" (seções 10-16) ---
|
| 362 |
+
df_para_ajuste = r[0] if r[0] is not None else df # df filtrado (sem outliers excluídos)
|
| 363 |
+
dropdown_vals = [transformacoes_x.get(colunas_x[i], "(x)") if i < len(colunas_x) else "(x)"
|
| 364 |
+
for i in range(MAX_VARS_X)]
|
| 365 |
+
m = ajustar_modelo_callback(df_para_ajuste, coluna_y, colunas_x, transformacao_y, outliers_excluidos, dicotomicas, codigo_alocado, percentuais, *dropdown_vals)
|
| 366 |
+
# m[0]:resultado [1]:diag [2]:coef [3]:obs_calc [4]:plot_disp [5]:dropdown
|
| 367 |
+
# m[6-10]:plots [11]:metricas [12]:est_metricas [13]:resumo
|
| 368 |
+
# m[14]:estado_avaliacoes
|
| 369 |
+
# m[15-16]:h10,a10 [17-18]:h11,a11 [19-20]:h12,a12 [21-22]:h13,a13
|
| 370 |
+
# m[23-24]:h14,a14 [25-26]:h15_aval,a15_aval [27-28]:h16_exp,a16_exp
|
| 371 |
+
# m[29-32]:filtro_vars
|
| 372 |
+
|
| 373 |
+
n_aval_rows = MAX_VARS_X // 4 # 5
|
| 374 |
+
|
| 375 |
+
return (
|
| 376 |
+
# --- Seções 1-3: dados básicos ---
|
| 377 |
+
df, # estado_df
|
| 378 |
+
msg, # status
|
| 379 |
+
gr.update(), # dropdown_aba (no-op)
|
| 380 |
+
gr.update(choices=colunas_numericas, value=coluna_y), # dropdown_y
|
| 381 |
+
arredondar_df(df), # tabela_dados
|
| 382 |
+
r[1], # tabela_estatisticas (de aplicar_selecao)
|
| 383 |
+
gr.update(choices=colunas_numericas, value=colunas_x), # checkboxes_x
|
| 384 |
+
mapa_html_val, # mapa
|
| 385 |
+
r[0], # estado_df_filtrado (de aplicar_selecao)
|
| 386 |
+
outliers_excluidos, # estado_outliers_anteriores
|
| 387 |
+
1, # estado_iteracao
|
| 388 |
+
gr.update(visible=bool(outliers_excluidos)), # accordion_outliers_anteriores
|
| 389 |
+
html_outliers_content, # html_outliers_anteriores
|
| 390 |
+
None, # estado_arquivo_temp
|
| 391 |
+
gr.update(visible=True, value=criar_header_secao(2, "Visualizar Dados", timestamp)),
|
| 392 |
+
gr.update(visible=True, open=True), # accordion_secao_2
|
| 393 |
+
gr.update(choices=["Visualização Padrão"] + colunas_numericas, value="Visualização Padrão"),
|
| 394 |
+
gr.update(visible=True), # header_secao_3
|
| 395 |
+
gr.update(visible=True, open=True), # accordion_secao_3
|
| 396 |
+
# --- States seções 4-16 ---
|
| 397 |
+
m[0], # estado_modelo (de ajustar)
|
| 398 |
+
m[12], # estado_metricas (de ajustar)
|
| 399 |
+
r[7], # estado_resultados_busca (de aplicar_selecao)
|
| 400 |
+
m[14], # estado_avaliacoes (de ajustar — reset [])
|
| 401 |
+
# --- Seção 4: seleção de variáveis ---
|
| 402 |
+
gr.update(visible=True, value=criar_header_secao(4, "Selecionar Variáveis Independentes", timestamp)),
|
| 403 |
+
gr.update(visible=True, open=True), # accordion_secao_4
|
| 404 |
+
gr.update(choices=list(colunas_x), value=list(dicotomicas), visible=True), # checkboxes_dicotomicas
|
| 405 |
+
gr.update(choices=list(colunas_x), value=list(codigo_alocado), visible=True), # checkboxes_codigo_alocado
|
| 406 |
+
gr.update(choices=list(colunas_x), value=list(percentuais), visible=True), # checkboxes_percentuais
|
| 407 |
+
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 408 |
+
# --- Seções 5-9: headers/accordions (de aplicar_selecao) ---
|
| 409 |
+
r[13], r[14], # header_secao_5, accordion_secao_5
|
| 410 |
+
r[15], r[16], # header_secao_6, accordion_secao_6
|
| 411 |
+
r[2], # html_micronumerosidade
|
| 412 |
+
r[17], r[18], # header_secao_7, accordion_secao_7
|
| 413 |
+
r[3], # plot_dispersao
|
| 414 |
+
r[19], r[20], # header_secao_8, accordion_secao_8
|
| 415 |
+
r[4], r[5], # slider_grau_coef, slider_grau_f
|
| 416 |
+
r[6], # busca_html
|
| 417 |
+
r[21], r[22], # header_secao_9, accordion_secao_9
|
| 418 |
+
gr.update(value=transformacao_y), # transformacao_y dropdown (do .dai)
|
| 419 |
+
r[8], r[9], r[10], r[11], r[12], # btn_adotar 1-5
|
| 420 |
+
# --- Campos de transformação (com dropdowns sobrescritos) ---
|
| 421 |
+
*campos_transf,
|
| 422 |
+
# --- Seções 10-16 (de ajustar_modelo_callback) ---
|
| 423 |
+
m[15], m[16], # header_secao_10, accordion_secao_10
|
| 424 |
+
m[5], # dropdown_tipo_grafico_dispersao
|
| 425 |
+
m[4], # plot_dispersao_transf
|
| 426 |
+
m[17], m[18], # header_secao_11, accordion_secao_11
|
| 427 |
+
m[1], # diagnosticos_html
|
| 428 |
+
m[2], # tabela_coef
|
| 429 |
+
m[3], # tabela_obs_calc
|
| 430 |
+
m[19], m[20], # header_secao_12, accordion_secao_12
|
| 431 |
+
m[6], m[7], m[8], m[9], m[10], # plots: obs_calc, resid, hist, cook, corr
|
| 432 |
+
m[21], m[22], # header_secao_13, accordion_secao_13
|
| 433 |
+
m[11], # tabela_metricas
|
| 434 |
+
m[23], m[24], # header_secao_14, accordion_secao_14
|
| 435 |
+
html_outliers_content, # html_outliers_sec14
|
| 436 |
+
"", # outliers_texto
|
| 437 |
+
"", # reincluir_texto
|
| 438 |
+
m[13], # txt_resumo_outliers
|
| 439 |
+
# --- Seção 15: Avaliação de Imóvel ---
|
| 440 |
+
m[25], m[26], # header_secao_15, accordion_secao_15
|
| 441 |
+
*[gr.update(visible=False) for _ in range(n_aval_rows)], # aval_rows (populados pelo .then)
|
| 442 |
+
*[gr.update(visible=False, value=None, label="") for _ in range(MAX_VARS_X)], # aval_inputs (populados pelo .then)
|
| 443 |
+
"", # resultado_avaliacao_html
|
| 444 |
+
gr.update(choices=[], value=None), # dropdown_base_avaliacao
|
| 445 |
+
"", # excluir_aval_trigger
|
| 446 |
+
gr.update(value=None, visible=False), # download_avaliacoes_file
|
| 447 |
+
# --- Seção 16: Exportar Modelo ---
|
| 448 |
+
m[27], m[28], # header_secao_16, accordion_secao_16
|
| 449 |
+
gr.update(value=""), # nome_arquivo
|
| 450 |
+
gr.update(value=""), # status_exportar
|
| 451 |
+
# Transição para estado C (pós-carregamento)
|
| 452 |
+
gr.update(visible=False), # row_upload
|
| 453 |
+
gr.update(visible=False), # row_selecao_aba
|
| 454 |
+
gr.update(visible=True), # row_pos_carga
|
| 455 |
+
gr.update(value=html_nome), # html_nome_arquivo_carregado
|
| 456 |
+
2, # estado_flag_carregamento (contador: 2 side-effects esperados)
|
| 457 |
+
m[29], m[30], m[31], m[32], # filtro_vars (choices com colunas originais)
|
| 458 |
+
# Painel de coordenadas (no-op — .dai isento)
|
| 459 |
+
gr.update(visible=False), gr.update(value=""),
|
| 460 |
+
gr.update(choices=[]), gr.update(choices=[]),
|
| 461 |
+
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 462 |
+
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 463 |
+
gr.update(visible=False), # row_opcao_mapear
|
| 464 |
+
gr.update(visible=False), # row_opcao_geocodificar
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
def carregar_dados_do_arquivo(caminho_arquivo, nome_aba, nome_arquivo_exibicao=""):
|
| 469 |
+
"""Carrega dados de um arquivo, opcionalmente de uma aba específica.
|
| 470 |
+
|
| 471 |
+
Args:
|
| 472 |
+
caminho_arquivo: Caminho do arquivo a carregar
|
| 473 |
+
nome_aba: Nome da aba a carregar (apenas para Excel)
|
| 474 |
+
nome_arquivo_exibicao: Nome do arquivo para exibir na interface pós-carregamento
|
| 475 |
+
|
| 476 |
+
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 477 |
+
"""
|
| 478 |
+
df, msg, sucesso = carregar_arquivo(caminho_arquivo, nome_aba)
|
| 479 |
+
|
| 480 |
+
if not sucesso:
|
| 481 |
+
return (
|
| 482 |
+
None, # estado_df
|
| 483 |
+
msg, # status
|
| 484 |
+
gr.update(), # dropdown_aba
|
| 485 |
+
gr.update(choices=[], value=None), # dropdown_y
|
| 486 |
+
None, # tabela_dados
|
| 487 |
+
None, # tabela_estatisticas
|
| 488 |
+
gr.update(choices=[]), # checkboxes_x
|
| 489 |
+
"<p>Carregue um arquivo para ver o mapa.</p>", # mapa
|
| 490 |
+
None, # estado_df_filtrado
|
| 491 |
+
[], # estado_outliers_anteriores
|
| 492 |
+
1, # estado_iteracao
|
| 493 |
+
gr.update(visible=False), # accordion_outliers_anteriores
|
| 494 |
+
"", # html_outliers_anteriores
|
| 495 |
+
None, # estado_arquivo_temp
|
| 496 |
+
# Seções 2 e 3 - ocultas quando erro
|
| 497 |
+
gr.update(visible=False), # header_secao_2
|
| 498 |
+
gr.update(visible=False), # accordion_secao_2
|
| 499 |
+
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"), # dropdown_mapa_var
|
| 500 |
+
gr.update(visible=False), # header_secao_3
|
| 501 |
+
gr.update(visible=False), # accordion_secao_3
|
| 502 |
+
# Reset seções 4-16
|
| 503 |
+
*_valores_reset_secoes_4_a_16(),
|
| 504 |
+
# Volta ao estado de upload em caso de erro
|
| 505 |
+
gr.update(visible=True), # row_upload
|
| 506 |
+
gr.update(visible=False), # row_selecao_aba
|
| 507 |
+
gr.update(visible=False), # row_pos_carga
|
| 508 |
+
gr.update(value=""), # html_nome_arquivo_carregado
|
| 509 |
+
False, # estado_flag_carregamento
|
| 510 |
+
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 511 |
+
# Painel de coordenadas (no-op — erro)
|
| 512 |
+
gr.update(visible=False), gr.update(value=""),
|
| 513 |
+
gr.update(choices=[]), gr.update(choices=[]),
|
| 514 |
+
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 515 |
+
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 516 |
+
gr.update(visible=False), # row_opcao_mapear
|
| 517 |
+
gr.update(visible=False), # row_opcao_geocodificar
|
| 518 |
+
)
|
| 519 |
+
|
| 520 |
+
# Verificar e padronizar coordenadas
|
| 521 |
+
tem_coords, col_lat, col_lon = verificar_coords(df)
|
| 522 |
+
todas_colunas = df.columns.tolist()
|
| 523 |
+
|
| 524 |
+
if tem_coords:
|
| 525 |
+
df = padronizar_coords(df, col_lat, col_lon)
|
| 526 |
+
aviso_coords_html = ""
|
| 527 |
+
cdlog_auto = None
|
| 528 |
+
num_auto = None
|
| 529 |
+
else:
|
| 530 |
+
cdlog_auto, num_auto = auto_detectar_colunas_geo(df)
|
| 531 |
+
n = len(df)
|
| 532 |
+
aviso_coords_html = (
|
| 533 |
+
'<div style="background:#f8d7da;border:1px solid #f5c2c7;border-radius:8px;'
|
| 534 |
+
f'padding:10px 14px;margin-bottom:8px"><strong>⚠️ Colunas lat/lon não '
|
| 535 |
+
f'encontradas</strong> — {n} registro(s) sem coordenadas padronizadas.<br>'
|
| 536 |
+
'</div>'
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
# Identifica colunas (após padronização de coords)
|
| 540 |
+
colunas_numericas = obter_colunas_numericas(df)
|
| 541 |
+
coluna_y_padrao = identificar_coluna_y_padrao(df)
|
| 542 |
+
|
| 543 |
+
# Variáveis X padrão: todas exceto Y
|
| 544 |
+
colunas_x_padrao = [col for col in colunas_numericas if col != coluna_y_padrao]
|
| 545 |
+
|
| 546 |
+
# NÃO calcula estatísticas no carregamento - apenas ao clicar em "Aplicar Seleção"
|
| 547 |
+
|
| 548 |
+
# Mapa
|
| 549 |
+
mapa_html = criar_mapa(df)
|
| 550 |
+
|
| 551 |
+
# Timestamp para seção 2
|
| 552 |
+
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 553 |
+
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 554 |
+
|
| 555 |
+
return (
|
| 556 |
+
df, # estado_df
|
| 557 |
+
msg, # status
|
| 558 |
+
gr.update(), # dropdown_aba (no-op)
|
| 559 |
+
gr.update(choices=colunas_numericas, value=coluna_y_padrao), # dropdown_y
|
| 560 |
+
arredondar_df(df), # tabela_dados
|
| 561 |
+
gr.update(value=None), # tabela_estatisticas - NÃO mostra até clicar em Aplicar Seleção
|
| 562 |
+
gr.update(choices=colunas_numericas, value=[]), # checkboxes_x (nenhuma marcada por padrão)
|
| 563 |
+
mapa_html, # mapa
|
| 564 |
+
df, # estado_df_filtrado (inicia com dados completos)
|
| 565 |
+
[], # estado_outliers_anteriores
|
| 566 |
+
1, # estado_iteracao
|
| 567 |
+
gr.update(visible=False), # accordion_outliers_anteriores
|
| 568 |
+
"", # html_outliers_anteriores
|
| 569 |
+
None, # estado_arquivo_temp
|
| 570 |
+
# Seções 2 e 3 — header sempre visível; accordion oculto até coords resolvidas
|
| 571 |
+
gr.update(visible=True, value=criar_header_secao(2, "Visualizar Dados", timestamp) if tem_coords else criar_header_secao(2, "Visualizar Dados")), # header_secao_2
|
| 572 |
+
gr.update(visible=tem_coords, open=True), # accordion_secao_2
|
| 573 |
+
gr.update(choices=["Visualização Padrão"] + colunas_numericas, value="Visualização Padrão"), # dropdown_mapa_var
|
| 574 |
+
gr.update(visible=True), # header_secao_3
|
| 575 |
+
gr.update(visible=tem_coords, open=True), # accordion_secao_3
|
| 576 |
+
# Reset seções 4-16
|
| 577 |
+
*_valores_reset_secoes_4_a_16(),
|
| 578 |
+
# Transição para estado C (pós-carregamento)
|
| 579 |
+
gr.update(visible=False), # row_upload
|
| 580 |
+
gr.update(visible=False), # row_selecao_aba
|
| 581 |
+
gr.update(visible=True), # row_pos_carga
|
| 582 |
+
gr.update(value=f"<h3>{nome_arquivo_exibicao}</h3>"), # html_nome_arquivo_carregado
|
| 583 |
+
2, # estado_flag_carregamento (contador: 2 side-effects esperados)
|
| 584 |
+
*[gr.update()] * 4, # filtro_vars (no-op — seções 13-14 ocultas)
|
| 585 |
+
# Painel de coordenadas (Estado D) — mostrado apenas se coords ausentes
|
| 586 |
+
gr.update(visible=not tem_coords), # row_coords_panel
|
| 587 |
+
gr.update(value=aviso_coords_html), # html_aviso_coords
|
| 588 |
+
gr.update(choices=todas_colunas if not tem_coords else []), # dropdown_col_lat_manual
|
| 589 |
+
gr.update(choices=todas_colunas if not tem_coords else []), # dropdown_col_lon_manual
|
| 590 |
+
gr.update(choices=todas_colunas if not tem_coords else [], value=cdlog_auto), # dropdown_cdlog_geo
|
| 591 |
+
gr.update(choices=todas_colunas if not tem_coords else [], value=num_auto), # dropdown_num_geo
|
| 592 |
+
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 593 |
+
gr.update(visible=False), # row_opcao_mapear
|
| 594 |
+
gr.update(visible=False), # row_opcao_geocodificar
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
# ============================================================
|
| 599 |
+
# LIMPAR HISTÓRICO
|
| 600 |
+
# ============================================================
|
| 601 |
+
|
| 602 |
+
def limpar_historico_callback(df_original):
|
| 603 |
+
"""Limpa o histórico de outliers e reinicia do zero.
|
| 604 |
+
Reseta tudo como se o arquivo tivesse acabado de ser carregado.
|
| 605 |
+
|
| 606 |
+
CONTRACT: Retorna 169 itens para btn_limpar_historico.click (app.py:criar_aba).
|
| 607 |
+
Se alterar, atualizar: app.py (outputs de btn_limpar_historico.click).
|
| 608 |
+
"""
|
| 609 |
+
mapa = criar_mapa(df_original)
|
| 610 |
+
|
| 611 |
+
return (
|
| 612 |
+
[], # estado_outliers_anteriores (vazio)
|
| 613 |
+
1, # estado_iteracao (reinicia)
|
| 614 |
+
df_original, # estado_df_filtrado (dados originais)
|
| 615 |
+
arredondar_df(df_original), # tabela_dados
|
| 616 |
+
gr.update(value=None), # tabela_estatisticas (limpa)
|
| 617 |
+
mapa, # mapa_html
|
| 618 |
+
"", # html_outliers_anteriores
|
| 619 |
+
gr.update(visible=False), # accordion_outliers_anteriores (esconde)
|
| 620 |
+
gr.update(value=criar_header_secao(2, "Visualizar Dados")), # header_secao_2
|
| 621 |
+
*_valores_reset_secoes_4_a_16(), # reset completo seções 4-15
|
| 622 |
+
*[gr.update()] * 4, # filtro_vars (no-op — seções ocultas após reset)
|
| 623 |
+
)
|
backend/app/core/elaboracao/charts.py
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
charts.py - Gráficos Plotly e mapa Folium para elaboração de modelos
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import plotly.graph_objects as go
|
| 9 |
+
from plotly.subplots import make_subplots
|
| 10 |
+
from scipy import stats
|
| 11 |
+
import folium
|
| 12 |
+
from folium import plugins
|
| 13 |
+
import branca.colormap as cm
|
| 14 |
+
|
| 15 |
+
# ============================================================
|
| 16 |
+
# CONSTANTES DE ESTILO
|
| 17 |
+
# ============================================================
|
| 18 |
+
|
| 19 |
+
COR_PRINCIPAL = '#FF8C00' # Laranja
|
| 20 |
+
COR_LINHA = '#dc3545' # Vermelho
|
| 21 |
+
COR_PONTOS = '#6c757d' # Cinza
|
| 22 |
+
COR_FUNDO = '#f8f9fa' # Cinza claro
|
| 23 |
+
COR_GRADE = '#e0e0e0' # Grade
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ============================================================
|
| 27 |
+
# GRÁFICOS DE DISPERSÃO X vs Y
|
| 28 |
+
# ============================================================
|
| 29 |
+
|
| 30 |
+
def criar_graficos_dispersao(X, y, max_por_linha=3):
|
| 31 |
+
"""
|
| 32 |
+
Cria gráficos de dispersão de cada coluna de X contra y.
|
| 33 |
+
Inclui reta de regressão e coeficiente de correlação.
|
| 34 |
+
|
| 35 |
+
Retorna figura Plotly.
|
| 36 |
+
"""
|
| 37 |
+
if X is None or y is None or X.empty:
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
n_colunas = min(max_por_linha, len(X.columns))
|
| 41 |
+
n_linhas = (len(X.columns) + n_colunas - 1) // n_colunas
|
| 42 |
+
|
| 43 |
+
y_nome = y.name if hasattr(y, 'name') and y.name else "y"
|
| 44 |
+
y_vals = pd.to_numeric(pd.Series(y), errors="coerce").to_numpy()
|
| 45 |
+
|
| 46 |
+
titulos = [f"{col} × {y_nome}" for col in X.columns]
|
| 47 |
+
|
| 48 |
+
fig = make_subplots(
|
| 49 |
+
rows=n_linhas,
|
| 50 |
+
cols=n_colunas,
|
| 51 |
+
subplot_titles=titulos,
|
| 52 |
+
horizontal_spacing=0.05,
|
| 53 |
+
vertical_spacing=0.06,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
indices_x = X.index.values
|
| 57 |
+
|
| 58 |
+
for idx, coluna in enumerate(X.columns):
|
| 59 |
+
x_vals = pd.to_numeric(X[coluna], errors="coerce").to_numpy()
|
| 60 |
+
|
| 61 |
+
mask = np.isfinite(x_vals) & np.isfinite(y_vals)
|
| 62 |
+
x_valid = x_vals[mask]
|
| 63 |
+
y_valid = y_vals[mask]
|
| 64 |
+
indices_valid = indices_x[mask]
|
| 65 |
+
|
| 66 |
+
if len(x_valid) < 2:
|
| 67 |
+
continue
|
| 68 |
+
|
| 69 |
+
row = (idx // n_colunas) + 1
|
| 70 |
+
col = (idx % n_colunas) + 1
|
| 71 |
+
|
| 72 |
+
x_unicos = np.unique(x_valid)
|
| 73 |
+
eh_dicotomica = len(x_unicos) <= 2
|
| 74 |
+
|
| 75 |
+
fig.add_trace(
|
| 76 |
+
go.Scatter(
|
| 77 |
+
x=x_valid,
|
| 78 |
+
y=y_valid,
|
| 79 |
+
mode='markers',
|
| 80 |
+
name=coluna,
|
| 81 |
+
marker=dict(size=7, color=COR_PONTOS, opacity=0.78),
|
| 82 |
+
customdata=indices_valid,
|
| 83 |
+
showlegend=False,
|
| 84 |
+
cliponaxis=False,
|
| 85 |
+
hovertemplate=f"<b>Índice:</b> %{{customdata}}<br><b>{coluna}:</b> %{{x:.2f}}<br><b>{y_nome}:</b> %{{y:.2f}}<extra></extra>",
|
| 86 |
+
),
|
| 87 |
+
row=row,
|
| 88 |
+
col=col,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
corr = np.nan
|
| 92 |
+
if np.nanstd(x_valid) > 0 and np.nanstd(y_valid) > 0:
|
| 93 |
+
corr = float(np.corrcoef(x_valid, y_valid)[0, 1])
|
| 94 |
+
|
| 95 |
+
# Para variáveis dicotômicas, não desenha reta de regressão no gráfico de inspeção.
|
| 96 |
+
if not eh_dicotomica and len(x_valid) >= 3 and np.nanstd(x_valid) > 0:
|
| 97 |
+
a, b = np.polyfit(x_valid, y_valid, 1)
|
| 98 |
+
x_sorted = np.sort(x_valid)
|
| 99 |
+
y_linha = a * x_sorted + b
|
| 100 |
+
fig.add_trace(
|
| 101 |
+
go.Scatter(
|
| 102 |
+
x=x_sorted,
|
| 103 |
+
y=y_linha,
|
| 104 |
+
mode='lines',
|
| 105 |
+
name=f'Regressão {coluna}',
|
| 106 |
+
line=dict(color=COR_PRINCIPAL, width=2),
|
| 107 |
+
showlegend=False,
|
| 108 |
+
hoverinfo='skip',
|
| 109 |
+
),
|
| 110 |
+
row=row,
|
| 111 |
+
col=col,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
if fig.layout.annotations and idx < len(fig.layout.annotations):
|
| 115 |
+
titulo = f"{coluna} × {y_nome}"
|
| 116 |
+
if np.isfinite(corr):
|
| 117 |
+
titulo = f"{titulo}<br>r = {corr:.3f}"
|
| 118 |
+
fig.layout.annotations[idx].text = titulo
|
| 119 |
+
|
| 120 |
+
x_min, x_max = float(np.min(x_valid)), float(np.max(x_valid))
|
| 121 |
+
if np.isclose(x_min, x_max):
|
| 122 |
+
x_pad = max(abs(x_min) * 0.15, 0.5)
|
| 123 |
+
elif eh_dicotomica:
|
| 124 |
+
x_pad = max((x_max - x_min) * 0.35, 0.30)
|
| 125 |
+
else:
|
| 126 |
+
x_pad = max((x_max - x_min) * 0.08, 0.05)
|
| 127 |
+
|
| 128 |
+
y_min, y_max = float(np.min(y_valid)), float(np.max(y_valid))
|
| 129 |
+
if np.isclose(y_min, y_max):
|
| 130 |
+
y_pad = max(abs(y_min) * 0.15, 0.5)
|
| 131 |
+
else:
|
| 132 |
+
y_pad = max((y_max - y_min) * 0.08, 0.05)
|
| 133 |
+
|
| 134 |
+
fig.update_xaxes(range=[x_min - x_pad, x_max + x_pad], row=row, col=col)
|
| 135 |
+
fig.update_yaxes(range=[y_min - y_pad, y_max + y_pad], row=row, col=col)
|
| 136 |
+
|
| 137 |
+
fig.update_layout(
|
| 138 |
+
height=max(380, 380 * n_linhas),
|
| 139 |
+
showlegend=False,
|
| 140 |
+
plot_bgcolor=COR_FUNDO,
|
| 141 |
+
paper_bgcolor='white',
|
| 142 |
+
margin=dict(t=78, r=20, b=48, l=54),
|
| 143 |
+
)
|
| 144 |
+
fig.update_annotations(showarrow=False)
|
| 145 |
+
|
| 146 |
+
fig.update_xaxes(showgrid=True, gridcolor=COR_GRADE)
|
| 147 |
+
fig.update_yaxes(showgrid=True, gridcolor=COR_GRADE)
|
| 148 |
+
|
| 149 |
+
return fig
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# ============================================================
|
| 153 |
+
# GRÁFICOS DE DIAGNÓSTICO DO MODELO
|
| 154 |
+
# ============================================================
|
| 155 |
+
|
| 156 |
+
def criar_grafico_obs_calc(y_obs, y_calc, indices=None):
|
| 157 |
+
"""Gráfico de valores observados vs calculados."""
|
| 158 |
+
if y_obs is None or y_calc is None:
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
y_obs = np.array(y_obs)
|
| 162 |
+
y_calc = np.array(y_calc)
|
| 163 |
+
|
| 164 |
+
fig = go.Figure()
|
| 165 |
+
|
| 166 |
+
# Pontos
|
| 167 |
+
scatter_args = dict(
|
| 168 |
+
x=y_calc,
|
| 169 |
+
y=y_obs,
|
| 170 |
+
mode='markers',
|
| 171 |
+
marker=dict(color=COR_PRINCIPAL, size=10, line=dict(color='black', width=1)),
|
| 172 |
+
name='Dados',
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
if indices is not None:
|
| 176 |
+
scatter_args['customdata'] = indices
|
| 177 |
+
scatter_args['hovertemplate'] = "<b>Índice:</b> %{customdata}<br><b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>"
|
| 178 |
+
else:
|
| 179 |
+
scatter_args['hovertemplate'] = "<b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>"
|
| 180 |
+
|
| 181 |
+
fig.add_trace(go.Scatter(**scatter_args))
|
| 182 |
+
|
| 183 |
+
# Linha de identidade
|
| 184 |
+
min_val = min(y_obs.min(), y_calc.min())
|
| 185 |
+
max_val = max(y_obs.max(), y_calc.max())
|
| 186 |
+
margin = (max_val - min_val) * 0.05
|
| 187 |
+
|
| 188 |
+
fig.add_trace(go.Scatter(
|
| 189 |
+
x=[min_val - margin, max_val + margin],
|
| 190 |
+
y=[min_val - margin, max_val + margin],
|
| 191 |
+
mode='lines',
|
| 192 |
+
line=dict(color=COR_LINHA, dash='dash', width=2),
|
| 193 |
+
name='Linha de identidade'
|
| 194 |
+
))
|
| 195 |
+
|
| 196 |
+
fig.update_layout(
|
| 197 |
+
title=dict(text='Observados vs Calculados', x=0.5),
|
| 198 |
+
xaxis_title='Valores Calculados',
|
| 199 |
+
yaxis_title='Valores Observados',
|
| 200 |
+
showlegend=True,
|
| 201 |
+
plot_bgcolor='white',
|
| 202 |
+
height=400
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 206 |
+
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 207 |
+
|
| 208 |
+
return fig
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def criar_grafico_residuos(y_calc, residuos, indices=None):
|
| 212 |
+
"""Gráfico de resíduos vs valores ajustados."""
|
| 213 |
+
if y_calc is None or residuos is None:
|
| 214 |
+
return None
|
| 215 |
+
|
| 216 |
+
y_calc = np.array(y_calc)
|
| 217 |
+
residuos = np.array(residuos)
|
| 218 |
+
|
| 219 |
+
fig = go.Figure()
|
| 220 |
+
|
| 221 |
+
scatter_args = dict(
|
| 222 |
+
x=y_calc,
|
| 223 |
+
y=residuos,
|
| 224 |
+
mode='markers',
|
| 225 |
+
marker=dict(color=COR_PRINCIPAL, size=10, line=dict(color='black', width=1)),
|
| 226 |
+
name='Resíduos',
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
if indices is not None:
|
| 230 |
+
scatter_args['customdata'] = indices
|
| 231 |
+
scatter_args['hovertemplate'] = "<b>Índice:</b> %{customdata}<br><b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>"
|
| 232 |
+
else:
|
| 233 |
+
scatter_args['hovertemplate'] = "<b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>"
|
| 234 |
+
|
| 235 |
+
fig.add_trace(go.Scatter(**scatter_args))
|
| 236 |
+
|
| 237 |
+
# Linhas de referência
|
| 238 |
+
fig.add_hline(y=0, line_dash="dash", line_color=COR_LINHA, line_width=2)
|
| 239 |
+
fig.add_hline(y=2, line_dash="dot", line_color='gray', line_width=1)
|
| 240 |
+
fig.add_hline(y=-2, line_dash="dot", line_color='gray', line_width=1)
|
| 241 |
+
|
| 242 |
+
fig.update_layout(
|
| 243 |
+
title=dict(text='Resíduos vs Valores Ajustados', x=0.5),
|
| 244 |
+
xaxis_title='Valores Ajustados',
|
| 245 |
+
yaxis_title='Resíduos Padronizados',
|
| 246 |
+
showlegend=False,
|
| 247 |
+
plot_bgcolor='white',
|
| 248 |
+
height=400
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 252 |
+
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 253 |
+
|
| 254 |
+
return fig
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def criar_histograma_residuos(residuos):
|
| 258 |
+
"""Histograma dos resíduos com curva normal."""
|
| 259 |
+
if residuos is None:
|
| 260 |
+
return None
|
| 261 |
+
|
| 262 |
+
residuos = np.array(residuos)
|
| 263 |
+
|
| 264 |
+
fig = go.Figure()
|
| 265 |
+
|
| 266 |
+
# Histograma
|
| 267 |
+
fig.add_trace(go.Histogram(
|
| 268 |
+
x=residuos,
|
| 269 |
+
histnorm='probability density',
|
| 270 |
+
marker=dict(color=COR_PRINCIPAL, line=dict(color='black', width=1)),
|
| 271 |
+
opacity=0.7,
|
| 272 |
+
name='Resíduos'
|
| 273 |
+
))
|
| 274 |
+
|
| 275 |
+
# Curva normal
|
| 276 |
+
mu, sigma = np.mean(residuos), np.std(residuos)
|
| 277 |
+
x_norm = np.linspace(residuos.min() - sigma, residuos.max() + sigma, 100)
|
| 278 |
+
y_norm = stats.norm.pdf(x_norm, mu, sigma)
|
| 279 |
+
|
| 280 |
+
fig.add_trace(go.Scatter(
|
| 281 |
+
x=x_norm,
|
| 282 |
+
y=y_norm,
|
| 283 |
+
mode='lines',
|
| 284 |
+
line=dict(color=COR_LINHA, width=3),
|
| 285 |
+
name='Curva Normal'
|
| 286 |
+
))
|
| 287 |
+
|
| 288 |
+
fig.update_layout(
|
| 289 |
+
title=dict(text='Distribuição dos Resíduos', x=0.5),
|
| 290 |
+
xaxis_title='Resíduos',
|
| 291 |
+
yaxis_title='Densidade',
|
| 292 |
+
showlegend=True,
|
| 293 |
+
plot_bgcolor='white',
|
| 294 |
+
barmode='overlay',
|
| 295 |
+
height=400
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 299 |
+
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 300 |
+
|
| 301 |
+
return fig
|
| 302 |
+
|
| 303 |
+
def criar_graficos_dispersao_residuos(X, residuos, max_por_linha=3):
|
| 304 |
+
"""
|
| 305 |
+
Cria gráficos de dispersão de cada coluna de X contra os resíduos padronizados.
|
| 306 |
+
Inclui linhas de referência em 0 e ±2.
|
| 307 |
+
|
| 308 |
+
Retorna figura Plotly.
|
| 309 |
+
"""
|
| 310 |
+
if X is None or residuos is None or X.empty:
|
| 311 |
+
return None
|
| 312 |
+
|
| 313 |
+
n_colunas = min(max_por_linha, len(X.columns))
|
| 314 |
+
n_linhas = (len(X.columns) + n_colunas - 1) // n_colunas
|
| 315 |
+
|
| 316 |
+
# Nome dos resíduos
|
| 317 |
+
y_nome = "Resíduo Padronizado"
|
| 318 |
+
y_vals = residuos
|
| 319 |
+
|
| 320 |
+
titulos = [f"{col} × {y_nome}" for col in X.columns]
|
| 321 |
+
|
| 322 |
+
fig = make_subplots(
|
| 323 |
+
rows=n_linhas,
|
| 324 |
+
cols=n_colunas,
|
| 325 |
+
subplot_titles=titulos,
|
| 326 |
+
horizontal_spacing=0.05,
|
| 327 |
+
vertical_spacing=0.06,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
# Obtém índices do DataFrame X
|
| 331 |
+
indices_x = X.index.values
|
| 332 |
+
|
| 333 |
+
for idx, coluna in enumerate(X.columns):
|
| 334 |
+
x_vals = X[coluna].values
|
| 335 |
+
|
| 336 |
+
# Alinha dados válidos
|
| 337 |
+
mask = ~(np.isnan(x_vals) | np.isnan(y_vals))
|
| 338 |
+
x_valid = x_vals[mask]
|
| 339 |
+
y_valid = y_vals[mask]
|
| 340 |
+
indices_valid = indices_x[mask]
|
| 341 |
+
|
| 342 |
+
if len(x_valid) < 1:
|
| 343 |
+
continue
|
| 344 |
+
|
| 345 |
+
row = (idx // n_colunas) + 1
|
| 346 |
+
col = (idx % n_colunas) + 1
|
| 347 |
+
|
| 348 |
+
# Pontos
|
| 349 |
+
fig.add_trace(
|
| 350 |
+
go.Scatter(
|
| 351 |
+
x=x_valid,
|
| 352 |
+
y=y_valid,
|
| 353 |
+
mode='markers',
|
| 354 |
+
name=coluna,
|
| 355 |
+
marker=dict(size=7, color=COR_PONTOS),
|
| 356 |
+
customdata=indices_valid,
|
| 357 |
+
showlegend=False,
|
| 358 |
+
hovertemplate=f"<b>Índice:</b> %{{customdata}}<br><b>{coluna}:</b> %{{x:.2f}}<br><b>Resíduo:</b> %{{y:.2f}}<extra></extra>"
|
| 359 |
+
),
|
| 360 |
+
row=row, col=col
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
# Linha horizontal em 0
|
| 364 |
+
fig.add_hline(y=0, line_dash="dash", line_color=COR_LINHA, line_width=2, row=row, col=col)
|
| 365 |
+
# Linhas em ±2
|
| 366 |
+
fig.add_hline(y=2, line_dash="dot", line_color='gray', line_width=1, row=row, col=col)
|
| 367 |
+
fig.add_hline(y=-2, line_dash="dot", line_color='gray', line_width=1, row=row, col=col)
|
| 368 |
+
|
| 369 |
+
fig.update_layout(
|
| 370 |
+
height=max(380, 380 * n_linhas),
|
| 371 |
+
showlegend=False,
|
| 372 |
+
plot_bgcolor=COR_FUNDO,
|
| 373 |
+
paper_bgcolor='white',
|
| 374 |
+
margin=dict(t=78, r=20, b=48, l=54),
|
| 375 |
+
)
|
| 376 |
+
fig.update_annotations(showarrow=False)
|
| 377 |
+
|
| 378 |
+
fig.update_xaxes(showgrid=True, gridcolor=COR_GRADE)
|
| 379 |
+
fig.update_yaxes(showgrid=True, gridcolor=COR_GRADE)
|
| 380 |
+
|
| 381 |
+
return fig
|
| 382 |
+
|
| 383 |
+
def criar_grafico_cook(cook_distances, indices=None, limite=None):
|
| 384 |
+
"""Gráfico de Distância de Cook."""
|
| 385 |
+
if cook_distances is None:
|
| 386 |
+
return None
|
| 387 |
+
|
| 388 |
+
cook = np.array(cook_distances)
|
| 389 |
+
n = len(cook)
|
| 390 |
+
|
| 391 |
+
# Usa índices fornecidos ou gera sequenciais
|
| 392 |
+
if indices is None:
|
| 393 |
+
indices = np.arange(1, n + 1)
|
| 394 |
+
else:
|
| 395 |
+
indices = np.array(indices)
|
| 396 |
+
|
| 397 |
+
if limite is None:
|
| 398 |
+
limite = 4 / n
|
| 399 |
+
|
| 400 |
+
fig = go.Figure()
|
| 401 |
+
|
| 402 |
+
# Hastes (usa posição sequencial para o eixo X, mas guarda índice real)
|
| 403 |
+
x_pos = np.arange(len(cook))
|
| 404 |
+
for i, (idx, valor) in enumerate(zip(indices, cook)):
|
| 405 |
+
cor = COR_LINHA if valor > limite else COR_PRINCIPAL
|
| 406 |
+
fig.add_trace(go.Scatter(
|
| 407 |
+
x=[i, i],
|
| 408 |
+
y=[0, valor],
|
| 409 |
+
mode='lines',
|
| 410 |
+
line=dict(color=cor, width=1.5),
|
| 411 |
+
showlegend=False,
|
| 412 |
+
hoverinfo='skip'
|
| 413 |
+
))
|
| 414 |
+
|
| 415 |
+
# Pontos
|
| 416 |
+
cores = [COR_LINHA if v > limite else COR_PRINCIPAL for v in cook]
|
| 417 |
+
fig.add_trace(go.Scatter(
|
| 418 |
+
x=x_pos,
|
| 419 |
+
y=cook,
|
| 420 |
+
mode='markers',
|
| 421 |
+
marker=dict(color=cores, size=8, line=dict(color='black', width=1)),
|
| 422 |
+
name='Distância de Cook',
|
| 423 |
+
showlegend=False,
|
| 424 |
+
customdata=indices,
|
| 425 |
+
hovertemplate='<b>Índice:</b> %{customdata}<br><b>Cook:</b> %{y:.4f}<extra></extra>'
|
| 426 |
+
))
|
| 427 |
+
|
| 428 |
+
# Linha de corte
|
| 429 |
+
fig.add_hline(
|
| 430 |
+
y=limite,
|
| 431 |
+
line_dash="dash",
|
| 432 |
+
line_color='gray',
|
| 433 |
+
annotation_text=f"4/n = {limite:.4f}",
|
| 434 |
+
annotation_position="top right"
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
fig.update_layout(
|
| 438 |
+
title=dict(text='Distância de Cook', x=0.5),
|
| 439 |
+
xaxis_title='Observação (ver índice no hover)',
|
| 440 |
+
yaxis_title='Distância de Cook',
|
| 441 |
+
showlegend=False,
|
| 442 |
+
plot_bgcolor='white',
|
| 443 |
+
height=400
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black', showticklabels=False)
|
| 447 |
+
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 448 |
+
|
| 449 |
+
return fig
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def criar_matriz_correlacao(df, colunas=None):
|
| 453 |
+
"""
|
| 454 |
+
Heatmap de correlação com estilo premium:
|
| 455 |
+
Diagonal limpa, linha divisória e cores customizadas.
|
| 456 |
+
"""
|
| 457 |
+
if df is None or df.empty:
|
| 458 |
+
return None
|
| 459 |
+
|
| 460 |
+
# Seleção de colunas e limpeza de dados constantes
|
| 461 |
+
if colunas:
|
| 462 |
+
df_corr = df[colunas].select_dtypes(include=[np.number])
|
| 463 |
+
else:
|
| 464 |
+
df_corr = df.select_dtypes(include=[np.number])
|
| 465 |
+
|
| 466 |
+
if df_corr.shape[1] < 2:
|
| 467 |
+
return None
|
| 468 |
+
|
| 469 |
+
# Garantir que não existam colunas com variância zero (evita NaNs na correlação)
|
| 470 |
+
variancias = df_corr.var(ddof=0)
|
| 471 |
+
df_corr = df_corr.loc[:, variancias.fillna(0) > 0]
|
| 472 |
+
|
| 473 |
+
if df_corr.shape[1] < 2:
|
| 474 |
+
return None
|
| 475 |
+
|
| 476 |
+
# Cálculo da correlação
|
| 477 |
+
corr = df_corr.corr()
|
| 478 |
+
|
| 479 |
+
# --- LÓGICA DE ESTILO DA PRIMEIRA FUNÇÃO ---
|
| 480 |
+
|
| 481 |
+
# 1. Remover diagonal (substitui por NaN para não colorir o Heatmap)
|
| 482 |
+
mask = np.eye(len(corr), dtype=bool)
|
| 483 |
+
corr_masked = corr.where(~mask)
|
| 484 |
+
|
| 485 |
+
# 2. Preparar texto (esconder NaNs da diagonal)
|
| 486 |
+
text = np.where(
|
| 487 |
+
np.isnan(corr_masked.values),
|
| 488 |
+
"",
|
| 489 |
+
np.round(corr_masked.values, 2).astype(str)
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# 3. Definição da Colorscale customizada (Divergente com centro branco largo)
|
| 493 |
+
custom_colorscale = [
|
| 494 |
+
[0.00, "rgb(103,0,31)"], [0.08, "rgb(178,24,43)"],
|
| 495 |
+
[0.16, "rgb(214,96,77)"], [0.24, "rgb(244,165,130)"],
|
| 496 |
+
[0.32, "rgb(253,219,199)"],
|
| 497 |
+
[0.45, "rgb(255,255,255)"], [0.55, "rgb(255,255,255)"], # Branco neutro
|
| 498 |
+
[0.68, "rgb(209,229,240)"], [0.76, "rgb(146,197,222)"],
|
| 499 |
+
[0.84, "rgb(67,147,195)"], [0.92, "rgb(33,102,172)"],
|
| 500 |
+
[1.00, "rgb(5,48,97)"]
|
| 501 |
+
]
|
| 502 |
+
|
| 503 |
+
fig = go.Figure(data=go.Heatmap(
|
| 504 |
+
z=corr_masked.values,
|
| 505 |
+
x=corr.columns,
|
| 506 |
+
y=corr.index,
|
| 507 |
+
text=text,
|
| 508 |
+
texttemplate="%{text}",
|
| 509 |
+
textfont=dict(size=10),
|
| 510 |
+
zmin=-1, zmax=1, zmid=0,
|
| 511 |
+
colorscale=custom_colorscale,
|
| 512 |
+
colorbar=dict(title='Correlação'),
|
| 513 |
+
hovertemplate="%{x} × %{y}<br>ρ = %{z:.3f}<extra></extra>"
|
| 514 |
+
))
|
| 515 |
+
|
| 516 |
+
# 4. Adicionar a linha diagonal visual
|
| 517 |
+
fig.add_shape(
|
| 518 |
+
type="line",
|
| 519 |
+
xref="paper", yref="paper",
|
| 520 |
+
x0=0, y0=1, x1=1, y1=0,
|
| 521 |
+
line=dict(color="rgba(0,0,0,0.35)", width=1),
|
| 522 |
+
layer="above"
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
fig.update_layout(
|
| 526 |
+
title=dict(text="Matriz de Correlação", x=0.5),
|
| 527 |
+
height=600,
|
| 528 |
+
template='plotly_white',
|
| 529 |
+
xaxis=dict(tickangle=45, showgrid=False),
|
| 530 |
+
yaxis=dict(autorange='reversed', showgrid=False)
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
return fig
|
| 534 |
+
|
| 535 |
+
# ============================================================
|
| 536 |
+
# GRÁFICOS COMBINADOS DE DIAGNÓSTICO
|
| 537 |
+
# ============================================================
|
| 538 |
+
|
| 539 |
+
def criar_painel_diagnostico(resultado_modelo):
|
| 540 |
+
"""
|
| 541 |
+
Cria painel com todos os gráficos de diagnóstico.
|
| 542 |
+
Retorna dict com figuras individuais.
|
| 543 |
+
"""
|
| 544 |
+
if resultado_modelo is None:
|
| 545 |
+
return {}
|
| 546 |
+
|
| 547 |
+
tabela = resultado_modelo.get("tabela_obs_calc")
|
| 548 |
+
if tabela is None:
|
| 549 |
+
return {}
|
| 550 |
+
|
| 551 |
+
# Extrai índices da tabela
|
| 552 |
+
indices = tabela["Índice"].values if "Índice" in tabela.columns else None
|
| 553 |
+
|
| 554 |
+
y_obs = tabela["Observado"].values if "Observado" in tabela.columns else None
|
| 555 |
+
y_calc = tabela["Calculado"].values if "Calculado" in tabela.columns else None
|
| 556 |
+
residuos_pad = tabela["Resíduo Pad."].values if "Resíduo Pad." in tabela.columns else None
|
| 557 |
+
cook = tabela["Cook"].values if "Cook" in tabela.columns else None
|
| 558 |
+
|
| 559 |
+
return {
|
| 560 |
+
"obs_calc": criar_grafico_obs_calc(y_obs, y_calc, indices),
|
| 561 |
+
"residuos": criar_grafico_residuos(y_calc, residuos_pad, indices),
|
| 562 |
+
"histograma": criar_histograma_residuos(residuos_pad),
|
| 563 |
+
"cook": criar_grafico_cook(cook, indices)
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
# ============================================================
|
| 568 |
+
# MAPA FOLIUM
|
| 569 |
+
# ============================================================
|
| 570 |
+
|
| 571 |
+
def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, indice_destacado=None, tamanho_col=None):
|
| 572 |
+
"""
|
| 573 |
+
Cria mapa Folium com os dados.
|
| 574 |
+
|
| 575 |
+
Parâmetros:
|
| 576 |
+
df: DataFrame com os dados
|
| 577 |
+
lat_col: nome da coluna de latitude
|
| 578 |
+
lon_col: nome da coluna de longitude
|
| 579 |
+
cor_col: coluna para colorir os pontos (opcional)
|
| 580 |
+
indice_destacado: índice do ponto a destacar (opcional)
|
| 581 |
+
tamanho_col: coluna numérica para dimensionar os círculos (opcional)
|
| 582 |
+
|
| 583 |
+
Retorna:
|
| 584 |
+
HTML do mapa
|
| 585 |
+
"""
|
| 586 |
+
# Verifica se colunas existem
|
| 587 |
+
cols_lower = {str(c).lower(): c for c in df.columns}
|
| 588 |
+
|
| 589 |
+
lat_real = None
|
| 590 |
+
lon_real = None
|
| 591 |
+
|
| 592 |
+
for nome in ["lat", "latitude", "siat_latitude"]:
|
| 593 |
+
if nome in cols_lower:
|
| 594 |
+
lat_real = cols_lower[nome]
|
| 595 |
+
break
|
| 596 |
+
|
| 597 |
+
for nome in ["lon", "longitude", "long", "siat_longitude"]:
|
| 598 |
+
if nome in cols_lower:
|
| 599 |
+
lon_real = cols_lower[nome]
|
| 600 |
+
break
|
| 601 |
+
|
| 602 |
+
if lat_real is None or lon_real is None:
|
| 603 |
+
return "<p>Coordenadas (lat/lon) não encontradas nos dados.</p>"
|
| 604 |
+
|
| 605 |
+
# Filtra dados válidos
|
| 606 |
+
df_mapa = df.dropna(subset=[lat_real, lon_real]).copy()
|
| 607 |
+
if df_mapa.empty:
|
| 608 |
+
return "<p>Sem coordenadas válidas para exibir.</p>"
|
| 609 |
+
|
| 610 |
+
# Cria mapa
|
| 611 |
+
centro_lat = df_mapa[lat_real].mean()
|
| 612 |
+
centro_lon = df_mapa[lon_real].mean()
|
| 613 |
+
|
| 614 |
+
m = folium.Map(
|
| 615 |
+
location=[centro_lat, centro_lon],
|
| 616 |
+
zoom_start=12,
|
| 617 |
+
tiles=None
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
# Camadas base
|
| 621 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True).add_to(m)
|
| 622 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True).add_to(m)
|
| 623 |
+
|
| 624 |
+
# Se tamanho_col fornecido mas cor_col não, usa mesma variável para cor
|
| 625 |
+
if tamanho_col and not cor_col:
|
| 626 |
+
cor_col = tamanho_col
|
| 627 |
+
|
| 628 |
+
# Colormap se houver coluna de cor (verde → vermelho)
|
| 629 |
+
colormap = None
|
| 630 |
+
if cor_col and cor_col in df_mapa.columns:
|
| 631 |
+
vmin = df_mapa[cor_col].min()
|
| 632 |
+
vmax = df_mapa[cor_col].max()
|
| 633 |
+
colormap = cm.LinearColormap(
|
| 634 |
+
colors=["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
|
| 635 |
+
vmin=vmin,
|
| 636 |
+
vmax=vmax,
|
| 637 |
+
caption=cor_col
|
| 638 |
+
)
|
| 639 |
+
colormap.add_to(m)
|
| 640 |
+
|
| 641 |
+
# Escala de tamanho proporcional
|
| 642 |
+
raio_min, raio_max = 3, 18
|
| 643 |
+
tamanho_func = None
|
| 644 |
+
if tamanho_col and tamanho_col in df_mapa.columns:
|
| 645 |
+
t_min = df_mapa[tamanho_col].min()
|
| 646 |
+
t_max = df_mapa[tamanho_col].max()
|
| 647 |
+
if t_max > t_min:
|
| 648 |
+
tamanho_func = lambda v, _min=t_min, _max=t_max: raio_min + (v - _min) / (_max - _min) * (raio_max - raio_min)
|
| 649 |
+
else:
|
| 650 |
+
tamanho_func = lambda v: (raio_min + raio_max) / 2
|
| 651 |
+
|
| 652 |
+
# Camada de índices (oculta por padrão, ativável pelo controle de camadas)
|
| 653 |
+
camada_indices = folium.FeatureGroup(name="Índices", show=False)
|
| 654 |
+
|
| 655 |
+
# Adiciona pontos
|
| 656 |
+
for idx, row in df_mapa.iterrows():
|
| 657 |
+
# Cor do ponto
|
| 658 |
+
if colormap and cor_col:
|
| 659 |
+
cor = colormap(row[cor_col])
|
| 660 |
+
else:
|
| 661 |
+
cor = COR_PRINCIPAL
|
| 662 |
+
|
| 663 |
+
# Calcula raio
|
| 664 |
+
if idx == indice_destacado:
|
| 665 |
+
raio = raio_max + 4
|
| 666 |
+
elif tamanho_func:
|
| 667 |
+
raio = tamanho_func(row[tamanho_col])
|
| 668 |
+
else:
|
| 669 |
+
raio = 4
|
| 670 |
+
peso = 3 if idx == indice_destacado else 1
|
| 671 |
+
|
| 672 |
+
# Popup com informações
|
| 673 |
+
popup_html = f"<b>Índice: {idx}</b><br>"
|
| 674 |
+
for col in df_mapa.columns:
|
| 675 |
+
if str(col).lower() not in ['lat', 'latitude', 'lon', 'longitude']:
|
| 676 |
+
val = row[col]
|
| 677 |
+
if isinstance(val, (int, float)):
|
| 678 |
+
popup_html += f"{col}: {val:.2f}<br>"
|
| 679 |
+
else:
|
| 680 |
+
popup_html += f"{col}: {val}<br>"
|
| 681 |
+
|
| 682 |
+
# Tooltip (hover): índice + variável selecionada no dropdown
|
| 683 |
+
tooltip_html = (
|
| 684 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px;"
|
| 685 |
+
" line-height:1.7; padding:2px 4px;'>"
|
| 686 |
+
f"<b>Índice {idx}</b>"
|
| 687 |
+
)
|
| 688 |
+
if tamanho_col and tamanho_col in df_mapa.columns:
|
| 689 |
+
val_t = row[tamanho_col]
|
| 690 |
+
val_str = f"{val_t:.2f}" if isinstance(val_t, (int, float)) else str(val_t)
|
| 691 |
+
tooltip_html += (
|
| 692 |
+
f"<br><span style='color:#555;'>{tamanho_col}:</span>"
|
| 693 |
+
f" <b>{val_str}</b>"
|
| 694 |
+
)
|
| 695 |
+
tooltip_html += "</div>"
|
| 696 |
+
|
| 697 |
+
folium.CircleMarker(
|
| 698 |
+
location=[row[lat_real], row[lon_real]],
|
| 699 |
+
radius=raio,
|
| 700 |
+
popup=folium.Popup(popup_html, max_width=300),
|
| 701 |
+
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 702 |
+
color='black',
|
| 703 |
+
weight=peso,
|
| 704 |
+
fill=True,
|
| 705 |
+
fillColor=cor,
|
| 706 |
+
fillOpacity=0.8 if idx == indice_destacado else 0.6
|
| 707 |
+
).add_to(m)
|
| 708 |
+
|
| 709 |
+
# Label com índice (na camada togglável)
|
| 710 |
+
folium.Marker(
|
| 711 |
+
location=[row[lat_real], row[lon_real]],
|
| 712 |
+
icon=folium.DivIcon(
|
| 713 |
+
html=f'<div style="font-size:16px; font-weight:bold; color:#333; text-align:center; line-height:{int(raio*2)}px; width:{int(raio*2)}px; margin-left:-{int(raio)}px; margin-top:-{int(raio)}px;">{idx}</div>',
|
| 714 |
+
icon_size=(int(raio*2), int(raio*2)),
|
| 715 |
+
icon_anchor=(int(raio), int(raio))
|
| 716 |
+
)
|
| 717 |
+
).add_to(camada_indices)
|
| 718 |
+
|
| 719 |
+
camada_indices.add_to(m)
|
| 720 |
+
|
| 721 |
+
# Controles
|
| 722 |
+
folium.LayerControl().add_to(m)
|
| 723 |
+
plugins.Fullscreen().add_to(m)
|
| 724 |
+
plugins.MeasureControl(
|
| 725 |
+
primary_length_unit='meters',
|
| 726 |
+
secondary_length_unit='kilometers',
|
| 727 |
+
primary_area_unit='sqmeters',
|
| 728 |
+
secondary_area_unit='hectares'
|
| 729 |
+
).add_to(m)
|
| 730 |
+
|
| 731 |
+
# Ajusta bounds
|
| 732 |
+
bounds = [
|
| 733 |
+
[df_mapa[lat_real].min(), df_mapa[lon_real].min()],
|
| 734 |
+
[df_mapa[lat_real].max(), df_mapa[lon_real].max()]
|
| 735 |
+
]
|
| 736 |
+
m.fit_bounds(bounds)
|
| 737 |
+
|
| 738 |
+
return m._repr_html_()
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
def criar_mapa_simples(df):
|
| 742 |
+
"""
|
| 743 |
+
Versão simplificada do mapa sem destaque.
|
| 744 |
+
"""
|
| 745 |
+
return criar_mapa(df, indice_destacado=None)
|
backend/app/core/elaboracao/core.py
ADDED
|
@@ -0,0 +1,2077 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
core.py - Lógica de negócio para elaboração de modelos estatísticos
|
| 4 |
+
Contém: carregamento de dados, estatísticas, transformações, modelo OLS, diagnósticos
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import pandas as pd
|
| 9 |
+
# Desabilita StringDtype para compatibilidade entre versões do pandas
|
| 10 |
+
pd.set_option('future.infer_string', False)
|
| 11 |
+
import numpy as np
|
| 12 |
+
import statsmodels.api as sm
|
| 13 |
+
from statsmodels.stats.diagnostic import het_breuschpagan
|
| 14 |
+
from statsmodels.stats.stattools import durbin_watson
|
| 15 |
+
from statsmodels.stats.outliers_influence import OLSInfluence
|
| 16 |
+
from scipy.stats import kstest
|
| 17 |
+
from itertools import product
|
| 18 |
+
from joblib import dump, load
|
| 19 |
+
import io
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ============================================================
|
| 23 |
+
# CONSTANTES
|
| 24 |
+
# ============================================================
|
| 25 |
+
|
| 26 |
+
TRANSFORMACOES = ["(x)", "1/(x)", "ln(x)", "exp(x)", "(x)^2", "raiz(x)", "1/raiz(x)"]
|
| 27 |
+
|
| 28 |
+
# Nomes comuns para identificar colunas especiais
|
| 29 |
+
NOMES_VUNIT = {"vunit", "vu", "valor_unitario", "vuloc", "vupriv", "vuaconst", "vuapriv", "valor unitário"}
|
| 30 |
+
NOMES_LAT = {"lat", "latitude", "y", "siat_latitude"}
|
| 31 |
+
NOMES_LON = {"lon", "longitude", "long", "x", "siat_longitude"}
|
| 32 |
+
|
| 33 |
+
# Tabela Durbin-Watson
|
| 34 |
+
DW_TABLE = {
|
| 35 |
+
0.05: {
|
| 36 |
+
1: {"n": [25, 50, 100, 200], "dL": [1.10, 1.32, 1.49, 1.57], "dU": [1.54, 1.62, 1.70, 1.75]},
|
| 37 |
+
2: {"n": [25, 50, 100, 200], "dL": [1.06, 1.30, 1.47, 1.55], "dU": [1.50, 1.60, 1.69, 1.74]},
|
| 38 |
+
3: {"n": [25, 50, 100, 200], "dL": [1.02, 1.28, 1.45, 1.54], "dU": [1.47, 1.58, 1.68, 1.73]},
|
| 39 |
+
4: {"n": [25, 50, 100, 200], "dL": [0.98, 1.26, 1.43, 1.52], "dU": [1.44, 1.56, 1.66, 1.72]},
|
| 40 |
+
},
|
| 41 |
+
0.01: {
|
| 42 |
+
1: {"n": [25, 50, 100, 200], "dL": [1.05, 1.32, 1.52, 1.66], "dU": [1.21, 1.40, 1.56, 1.68]},
|
| 43 |
+
2: {"n": [25, 50, 100, 200], "dL": [0.98, 1.28, 1.50, 1.65], "dU": [1.30, 1.45, 1.58, 1.69]},
|
| 44 |
+
3: {"n": [25, 50, 100, 200], "dL": [0.90, 1.24, 1.48, 1.64], "dU": [1.41, 1.49, 1.60, 1.70]},
|
| 45 |
+
4: {"n": [25, 50, 100, 200], "dL": [0.83, 1.20, 1.46, 1.63], "dU": [1.52, 1.54, 1.63, 1.72]},
|
| 46 |
+
},
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ============================================================
|
| 51 |
+
# CARREGAMENTO DE DADOS
|
| 52 |
+
# ============================================================
|
| 53 |
+
|
| 54 |
+
def detectar_abas_excel(arquivo):
|
| 55 |
+
"""
|
| 56 |
+
Detecta as abas (sheets) de um arquivo Excel.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
arquivo: Pode ser um objeto de arquivo ou um caminho (string)
|
| 60 |
+
|
| 61 |
+
Retorna:
|
| 62 |
+
tuple: (lista_abas, mensagem, sucesso)
|
| 63 |
+
"""
|
| 64 |
+
if arquivo is None:
|
| 65 |
+
return [], "Nenhum arquivo enviado.", False
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
# Obtém o caminho/nome do arquivo
|
| 69 |
+
caminho = arquivo.name if hasattr(arquivo, 'name') else str(arquivo)
|
| 70 |
+
|
| 71 |
+
if caminho.endswith(('.xlsx', '.xls')):
|
| 72 |
+
# Usa o caminho para abrir o arquivo (funciona com path ou file object)
|
| 73 |
+
excel_file = pd.ExcelFile(caminho)
|
| 74 |
+
abas = excel_file.sheet_names
|
| 75 |
+
return abas, f"Arquivo com {len(abas)} aba(s) detectada(s)", True
|
| 76 |
+
else:
|
| 77 |
+
# CSV não tem abas
|
| 78 |
+
return [], "Arquivo CSV (sem abas)", True
|
| 79 |
+
|
| 80 |
+
except Exception as e:
|
| 81 |
+
return [], f"Erro ao detectar abas: {str(e)}", False
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def carregar_arquivo(arquivo, nome_aba=None):
|
| 85 |
+
"""
|
| 86 |
+
Carrega arquivo Excel ou CSV e retorna DataFrame.
|
| 87 |
+
Detecta automaticamente o separador do CSV.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
arquivo: Arquivo a ser carregado (objeto de arquivo ou caminho string)
|
| 91 |
+
nome_aba: Nome da aba a carregar (apenas para Excel). Se None, carrega a primeira.
|
| 92 |
+
|
| 93 |
+
Retorna:
|
| 94 |
+
tuple: (df, mensagem, sucesso)
|
| 95 |
+
"""
|
| 96 |
+
if arquivo is None:
|
| 97 |
+
return None, "Nenhum arquivo enviado.", False
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
# Obtém o caminho do arquivo
|
| 101 |
+
caminho = arquivo.name if hasattr(arquivo, 'name') else str(arquivo)
|
| 102 |
+
|
| 103 |
+
if caminho.endswith(('.xlsx', '.xls')):
|
| 104 |
+
# Carrega aba específica ou a primeira (usa caminho para poder reabrir)
|
| 105 |
+
df = pd.read_excel(caminho, sheet_name=nome_aba if nome_aba else 0)
|
| 106 |
+
aba_info = f" (aba: {nome_aba})" if nome_aba else ""
|
| 107 |
+
elif caminho.endswith('.csv'):
|
| 108 |
+
# Tenta detectar separador
|
| 109 |
+
with open(caminho, 'rb') as f:
|
| 110 |
+
content = f.read()
|
| 111 |
+
|
| 112 |
+
# Tenta diferentes separadores
|
| 113 |
+
for sep in [',', ';', '\t']:
|
| 114 |
+
try:
|
| 115 |
+
df = pd.read_csv(io.BytesIO(content), sep=sep)
|
| 116 |
+
if len(df.columns) > 1:
|
| 117 |
+
break
|
| 118 |
+
except:
|
| 119 |
+
continue
|
| 120 |
+
else:
|
| 121 |
+
df = pd.read_csv(io.BytesIO(content))
|
| 122 |
+
aba_info = ""
|
| 123 |
+
else:
|
| 124 |
+
return None, "Formato de arquivo não suportado.", False
|
| 125 |
+
|
| 126 |
+
# Reinicia índice começando em 1
|
| 127 |
+
df = df.reset_index(drop=True)
|
| 128 |
+
df.index = df.index + 1
|
| 129 |
+
|
| 130 |
+
# Garante nomes de colunas como string (evita mismatch com colunas numéricas, ex: anos)
|
| 131 |
+
df.columns = df.columns.astype(str)
|
| 132 |
+
|
| 133 |
+
return df, f"Arquivo carregado: {os.path.basename(caminho)}{aba_info} ({len(df)} linhas, {len(df.columns)} colunas)", True
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
return None, f"Erro ao carregar arquivo: {str(e)}", False
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _migrar_pacote_v1_para_v2(pacote):
|
| 140 |
+
"""Converte pacote .dai v1 (flat) para estrutura v2 (nested)."""
|
| 141 |
+
resumo = pacote.get("modelos_resumos", {})
|
| 142 |
+
return {
|
| 143 |
+
"versao": 2,
|
| 144 |
+
"dados": {
|
| 145 |
+
"df": pacote["Xy_preview_out_coords"],
|
| 146 |
+
"estatisticas": pacote["estatisticas"],
|
| 147 |
+
},
|
| 148 |
+
"transformacoes": {
|
| 149 |
+
"info": pacote["formatted_top_transformation_info"],
|
| 150 |
+
"X": pacote["top_X_esc"],
|
| 151 |
+
"y": pacote["top_y_esc"],
|
| 152 |
+
"dicotomicas": pacote.get("dicotomicas", []),
|
| 153 |
+
},
|
| 154 |
+
"modelo": {
|
| 155 |
+
"sm": pacote["modelos_sm"],
|
| 156 |
+
"coeficientes": pacote["tabelas_coef"],
|
| 157 |
+
"obs_calc": pacote["tabelas_obs_calc"],
|
| 158 |
+
"diagnosticos": {
|
| 159 |
+
"gerais": {
|
| 160 |
+
"n": resumo.get("n"),
|
| 161 |
+
"k": resumo.get("k"),
|
| 162 |
+
"desvio_padrao_residuos": resumo.get("desvio_padrao_residuos"),
|
| 163 |
+
"mse": resumo.get("mse"),
|
| 164 |
+
"r2": resumo.get("r2"),
|
| 165 |
+
"r2_ajustado": resumo.get("r2_ajustado"),
|
| 166 |
+
"r_pearson": resumo.get("r_pearson"),
|
| 167 |
+
},
|
| 168 |
+
"teste_f": {
|
| 169 |
+
"estatistica": resumo.get("Fc"),
|
| 170 |
+
"p_valor": resumo.get("p_valor_F"),
|
| 171 |
+
"interpretacao": resumo.get("Interpretacao_F"),
|
| 172 |
+
},
|
| 173 |
+
"teste_ks": {
|
| 174 |
+
"estatistica": resumo.get("ks_stat"),
|
| 175 |
+
"p_valor": resumo.get("ks_p"),
|
| 176 |
+
"interpretacao": resumo.get("Interpretacao_KS"),
|
| 177 |
+
},
|
| 178 |
+
"teste_normalidade": {
|
| 179 |
+
"percentuais": resumo.get("perc_resid"),
|
| 180 |
+
},
|
| 181 |
+
"teste_dw": {
|
| 182 |
+
"estatistica": resumo.get("dw"),
|
| 183 |
+
"interpretacao": resumo.get("Interpretacao_DW"),
|
| 184 |
+
},
|
| 185 |
+
"teste_bp": {
|
| 186 |
+
"estatistica": resumo.get("bp_lm"),
|
| 187 |
+
"p_valor": resumo.get("bp_p"),
|
| 188 |
+
"interpretacao": resumo.get("Interpretacao_BP"),
|
| 189 |
+
},
|
| 190 |
+
"equacao": resumo.get("equacao"),
|
| 191 |
+
},
|
| 192 |
+
},
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def carregar_dai(caminho):
|
| 197 |
+
"""
|
| 198 |
+
Carrega arquivo .dai e extrai DataFrame, variáveis e transformações.
|
| 199 |
+
|
| 200 |
+
Retorna:
|
| 201 |
+
tuple: (df, coluna_y, colunas_x, transformacao_y, transformacoes_x, dicotomicas, codigo_alocado, percentuais, mensagem, sucesso)
|
| 202 |
+
"""
|
| 203 |
+
try:
|
| 204 |
+
pacote = load(caminho)
|
| 205 |
+
|
| 206 |
+
# Retrocompatibilidade: converte v1 (flat) para v2 (nested)
|
| 207 |
+
if "versao" not in pacote:
|
| 208 |
+
pacote = _migrar_pacote_v1_para_v2(pacote)
|
| 209 |
+
|
| 210 |
+
# Extrai novos campos (se existirem)
|
| 211 |
+
df_completo = pacote["dados"].get("df_completo", None)
|
| 212 |
+
outliers_excluidos = pacote["dados"].get("outliers_excluidos", [])
|
| 213 |
+
|
| 214 |
+
# Extrai DataFrame — preserva índices originais se df_completo disponível
|
| 215 |
+
if df_completo is not None:
|
| 216 |
+
# .dai novo: usa DataFrame completo com índices originais (sem reset!)
|
| 217 |
+
df = df_completo.copy()
|
| 218 |
+
else:
|
| 219 |
+
# .dai antigo: usa df filtrado e renumera índices (retrocompat)
|
| 220 |
+
df = pacote["dados"]["df"].copy()
|
| 221 |
+
df = df.reset_index(drop=True)
|
| 222 |
+
df.index = df.index + 1
|
| 223 |
+
|
| 224 |
+
# Normaliza nomes de colunas para string (evita mismatch com colunas numéricas, ex: anos)
|
| 225 |
+
df.columns = df.columns.astype(str)
|
| 226 |
+
|
| 227 |
+
# Parseia transformações
|
| 228 |
+
info_transf = pacote["transformacoes"]["info"]
|
| 229 |
+
|
| 230 |
+
# Primeiro elemento: Y (ex: "VUNIT: ln(y)")
|
| 231 |
+
nome_y, transf_y = info_transf[0].split(": ", 1)
|
| 232 |
+
coluna_y = nome_y.strip()
|
| 233 |
+
transformacao_y = transf_y.strip().replace("(y)", "(x)")
|
| 234 |
+
|
| 235 |
+
# Demais: X (ex: "Area: ln(x)")
|
| 236 |
+
colunas_x = []
|
| 237 |
+
transformacoes_x = {}
|
| 238 |
+
for item in info_transf[1:]:
|
| 239 |
+
nome_x, transf_x = item.split(": ", 1)
|
| 240 |
+
nome_x = nome_x.strip()
|
| 241 |
+
colunas_x.append(nome_x)
|
| 242 |
+
transformacoes_x[nome_x] = transf_x.strip()
|
| 243 |
+
|
| 244 |
+
# Extrai dicotômicas, código alocado e percentuais (3 listas separadas)
|
| 245 |
+
dicotomicas = pacote["transformacoes"].get("dicotomicas", None)
|
| 246 |
+
codigo_alocado_salvo = pacote["transformacoes"].get("codigo_alocado", None)
|
| 247 |
+
percentuais_salvo = pacote["transformacoes"].get("percentuais", None)
|
| 248 |
+
|
| 249 |
+
# Normaliza para string (colunas numéricas ficam como inteiros quando serializadas diretamente)
|
| 250 |
+
if dicotomicas is not None:
|
| 251 |
+
dicotomicas = [str(c) for c in dicotomicas]
|
| 252 |
+
if codigo_alocado_salvo is not None:
|
| 253 |
+
codigo_alocado_salvo = [str(c) for c in codigo_alocado_salvo]
|
| 254 |
+
if percentuais_salvo is not None:
|
| 255 |
+
percentuais_salvo = [str(c) for c in percentuais_salvo]
|
| 256 |
+
|
| 257 |
+
if dicotomicas is None:
|
| 258 |
+
# Retrocompat: .dai antigos sem nenhuma info
|
| 259 |
+
dicotomicas = [col for col in colunas_x
|
| 260 |
+
if set(df[col].dropna().unique()).issubset({0, 1, 0.0, 1.0})]
|
| 261 |
+
codigo_alocado_salvo = detectar_codigo_alocado(df, colunas_x)
|
| 262 |
+
percentuais_salvo = detectar_percentuais(df, colunas_x)
|
| 263 |
+
elif codigo_alocado_salvo is None:
|
| 264 |
+
# Retrocompat: .dai com "dicotomicas" misturadas (dic+cod), sem separação
|
| 265 |
+
reais_01 = [c for c in dicotomicas if set(df[c].dropna().unique()).issubset({0, 1, 0.0, 1.0})]
|
| 266 |
+
codigo_alocado_salvo = [c for c in dicotomicas if c not in reais_01]
|
| 267 |
+
dicotomicas = reais_01
|
| 268 |
+
percentuais_salvo = detectar_percentuais(df, colunas_x)
|
| 269 |
+
|
| 270 |
+
elaborador = pacote.get("elaborador", None)
|
| 271 |
+
msg = f"Modelo .dai carregado: {os.path.basename(caminho)} ({len(df)} dados, Y={coluna_y}, {len(colunas_x)} variáveis X)"
|
| 272 |
+
return df, coluna_y, colunas_x, transformacao_y, transformacoes_x, dicotomicas, codigo_alocado_salvo or [], percentuais_salvo or [], msg, True, elaborador, outliers_excluidos
|
| 273 |
+
|
| 274 |
+
except Exception as e:
|
| 275 |
+
return None, None, None, None, None, [], [], [], f"Erro ao carregar .dai: {str(e)}", False, None, []
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def identificar_coluna_y_padrao(df):
|
| 279 |
+
"""
|
| 280 |
+
Identifica a coluna padrão para variável dependente (y).
|
| 281 |
+
Prioriza VUNIT se existir.
|
| 282 |
+
"""
|
| 283 |
+
colunas_lower = {str(col).lower(): col for col in df.columns}
|
| 284 |
+
|
| 285 |
+
for nome in NOMES_VUNIT:
|
| 286 |
+
if nome in colunas_lower:
|
| 287 |
+
return colunas_lower[nome]
|
| 288 |
+
|
| 289 |
+
# Se não encontrar, retorna primeira coluna numérica
|
| 290 |
+
numericas = df.select_dtypes(include=[np.number]).columns.tolist()
|
| 291 |
+
return numericas[0] if numericas else None
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def identificar_colunas_coords(df):
|
| 295 |
+
"""
|
| 296 |
+
Identifica colunas de latitude e longitude.
|
| 297 |
+
Retorna tuple (lat_col, lon_col) ou (None, None).
|
| 298 |
+
"""
|
| 299 |
+
colunas_lower = {str(col).lower(): col for col in df.columns}
|
| 300 |
+
|
| 301 |
+
lat_col = None
|
| 302 |
+
lon_col = None
|
| 303 |
+
|
| 304 |
+
for nome in NOMES_LAT:
|
| 305 |
+
if nome in colunas_lower:
|
| 306 |
+
lat_col = colunas_lower[nome]
|
| 307 |
+
break
|
| 308 |
+
|
| 309 |
+
for nome in NOMES_LON:
|
| 310 |
+
if nome in colunas_lower:
|
| 311 |
+
lon_col = colunas_lower[nome]
|
| 312 |
+
break
|
| 313 |
+
|
| 314 |
+
return lat_col, lon_col
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def obter_colunas_numericas(df, excluir_coords=True):
|
| 318 |
+
"""
|
| 319 |
+
Retorna lista de colunas numéricas.
|
| 320 |
+
Opcionalmente exclui lat/lon.
|
| 321 |
+
"""
|
| 322 |
+
numericas = df.select_dtypes(include=[np.number]).columns.tolist()
|
| 323 |
+
|
| 324 |
+
if excluir_coords:
|
| 325 |
+
lat_col, lon_col = identificar_colunas_coords(df)
|
| 326 |
+
if lat_col and lat_col in numericas:
|
| 327 |
+
numericas.remove(lat_col)
|
| 328 |
+
if lon_col and lon_col in numericas:
|
| 329 |
+
numericas.remove(lon_col)
|
| 330 |
+
|
| 331 |
+
return numericas
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
# ============================================================
|
| 335 |
+
# ESTATÍSTICAS
|
| 336 |
+
# ============================================================
|
| 337 |
+
|
| 338 |
+
def calcular_estatisticas_variaveis(df, coluna_y=None, indices_usar=None, colunas=None):
|
| 339 |
+
"""
|
| 340 |
+
Calcula estatísticas para cada coluna numérica.
|
| 341 |
+
Se coluna_y for especificada, inclui correlação com y.
|
| 342 |
+
Se colunas for especificada, calcula apenas para essas colunas.
|
| 343 |
+
|
| 344 |
+
Parâmetros:
|
| 345 |
+
df: DataFrame com os dados
|
| 346 |
+
coluna_y: nome da coluna da variável dependente (opcional)
|
| 347 |
+
indices_usar: lista de índices a incluir (se None, usa todos)
|
| 348 |
+
colunas: lista de colunas a calcular (se None, usa todas numéricas)
|
| 349 |
+
|
| 350 |
+
Retorna DataFrame com estatísticas.
|
| 351 |
+
"""
|
| 352 |
+
# Filtra por índices se especificado
|
| 353 |
+
df_calc = df.copy()
|
| 354 |
+
if indices_usar is not None:
|
| 355 |
+
df_calc = df_calc.loc[indices_usar]
|
| 356 |
+
|
| 357 |
+
# Usa colunas especificadas ou todas numéricas
|
| 358 |
+
if colunas is not None:
|
| 359 |
+
numericas = [c for c in colunas if c in df_calc.columns and pd.api.types.is_numeric_dtype(df_calc[c])]
|
| 360 |
+
else:
|
| 361 |
+
numericas = obter_colunas_numericas(df_calc)
|
| 362 |
+
|
| 363 |
+
estatisticas = []
|
| 364 |
+
|
| 365 |
+
for col in numericas:
|
| 366 |
+
serie = df_calc[col].dropna()
|
| 367 |
+
|
| 368 |
+
stats = {
|
| 369 |
+
"Variável": col,
|
| 370 |
+
"Contagem": len(serie),
|
| 371 |
+
"Média": serie.mean(),
|
| 372 |
+
"Mediana": serie.median(),
|
| 373 |
+
"Mínimo": serie.min(),
|
| 374 |
+
"Máximo": serie.max(),
|
| 375 |
+
"Desvio Padrão": serie.std(),
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
# Coeficiente de variação
|
| 379 |
+
if stats["Média"] != 0:
|
| 380 |
+
stats["CV (%)"] = abs(stats["Desvio Padrão"] / stats["Média"]) * 100
|
| 381 |
+
else:
|
| 382 |
+
stats["CV (%)"] = np.nan
|
| 383 |
+
|
| 384 |
+
# Correlação com y
|
| 385 |
+
if coluna_y and col != coluna_y:
|
| 386 |
+
y_serie = df_calc[coluna_y].dropna()
|
| 387 |
+
comum = serie.index.intersection(y_serie.index)
|
| 388 |
+
if len(comum) > 2:
|
| 389 |
+
stats["Correlação (y)"] = np.corrcoef(serie.loc[comum], y_serie.loc[comum])[0, 1]
|
| 390 |
+
else:
|
| 391 |
+
stats["Correlação (y)"] = np.nan
|
| 392 |
+
else:
|
| 393 |
+
stats["Correlação (y)"] = np.nan if col != coluna_y else 1.0
|
| 394 |
+
|
| 395 |
+
# Detecta se é dicotômica
|
| 396 |
+
valores_unicos = set(serie.unique())
|
| 397 |
+
stats["Dicotômica"] = valores_unicos.issubset({0, 1, 0.0, 1.0})
|
| 398 |
+
|
| 399 |
+
estatisticas.append(stats)
|
| 400 |
+
|
| 401 |
+
df_stats = pd.DataFrame(estatisticas)
|
| 402 |
+
|
| 403 |
+
# Ordena por correlação absoluta (maiores primeiro)
|
| 404 |
+
if coluna_y:
|
| 405 |
+
df_stats["_abs_corr"] = df_stats["Correlação (y)"].abs()
|
| 406 |
+
df_stats = df_stats.sort_values("_abs_corr", ascending=False)
|
| 407 |
+
df_stats = df_stats.drop(columns=["_abs_corr"])
|
| 408 |
+
|
| 409 |
+
return df_stats.reset_index(drop=True)
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
# ============================================================
|
| 413 |
+
# ANÁLISE DE OUTLIERS
|
| 414 |
+
# ============================================================
|
| 415 |
+
|
| 416 |
+
def calcular_metricas_outliers(df, coluna_y, colunas_x, indices_participantes=None):
|
| 417 |
+
"""
|
| 418 |
+
Ajusta modelo simples (sem transformações) para calcular métricas de outliers.
|
| 419 |
+
|
| 420 |
+
Parâmetros:
|
| 421 |
+
df: DataFrame com os dados
|
| 422 |
+
coluna_y: nome da coluna da variável dependente
|
| 423 |
+
colunas_x: lista de nomes das colunas independentes
|
| 424 |
+
indices_participantes: lista de índices a incluir (se None, usa todos)
|
| 425 |
+
|
| 426 |
+
Retorna:
|
| 427 |
+
DataFrame com métricas: Índice, Observado, Calculado, Resíduo,
|
| 428 |
+
Resíduo Pad., Resíduo Stud., Cook
|
| 429 |
+
"""
|
| 430 |
+
if not colunas_x:
|
| 431 |
+
return None
|
| 432 |
+
|
| 433 |
+
# Filtra por participantes
|
| 434 |
+
df_calc = df.copy()
|
| 435 |
+
if indices_participantes is not None:
|
| 436 |
+
df_calc = df_calc.loc[indices_participantes]
|
| 437 |
+
|
| 438 |
+
# Prepara y (sem transformação)
|
| 439 |
+
y = df_calc[coluna_y].values
|
| 440 |
+
|
| 441 |
+
# Prepara X (sem transformação)
|
| 442 |
+
X = df_calc[colunas_x].copy()
|
| 443 |
+
|
| 444 |
+
# Remove linhas com NaN ou Inf
|
| 445 |
+
mask = ~(np.isnan(y) | np.isinf(y))
|
| 446 |
+
for col in X.columns:
|
| 447 |
+
mask &= ~(np.isnan(X[col]) | np.isinf(X[col]))
|
| 448 |
+
|
| 449 |
+
y_final = y[mask]
|
| 450 |
+
X_final = X.loc[mask.values if hasattr(mask, 'values') else mask]
|
| 451 |
+
indices_final = df_calc.index[mask]
|
| 452 |
+
|
| 453 |
+
if len(y_final) < len(colunas_x) + 2:
|
| 454 |
+
return None
|
| 455 |
+
|
| 456 |
+
try:
|
| 457 |
+
# Ajusta modelo simples
|
| 458 |
+
X_sm = sm.add_constant(X_final)
|
| 459 |
+
modelo = sm.OLS(y_final, X_sm).fit()
|
| 460 |
+
|
| 461 |
+
# Calcula métricas
|
| 462 |
+
residuos = modelo.resid
|
| 463 |
+
n = len(y_final)
|
| 464 |
+
k = len(colunas_x)
|
| 465 |
+
desvio_padrao = np.sqrt(np.sum(residuos**2) / (n - k - 1))
|
| 466 |
+
residuos_pad = residuos / desvio_padrao
|
| 467 |
+
|
| 468 |
+
influence = OLSInfluence(modelo)
|
| 469 |
+
residuos_stud = influence.resid_studentized
|
| 470 |
+
cook = influence.cooks_distance[0]
|
| 471 |
+
|
| 472 |
+
# Monta DataFrame
|
| 473 |
+
df_metricas = pd.DataFrame({
|
| 474 |
+
"Índice": indices_final,
|
| 475 |
+
"Observado": y_final,
|
| 476 |
+
"Calculado": modelo.predict(),
|
| 477 |
+
"Resíduo": residuos,
|
| 478 |
+
"Resíduo Pad.": residuos_pad,
|
| 479 |
+
"Resíduo Stud.": residuos_stud,
|
| 480 |
+
"Cook": cook
|
| 481 |
+
})
|
| 482 |
+
df_metricas = df_metricas.set_index("Índice")
|
| 483 |
+
|
| 484 |
+
return df_metricas
|
| 485 |
+
|
| 486 |
+
except Exception as e:
|
| 487 |
+
print(f"Erro ao calcular métricas de outliers: {e}")
|
| 488 |
+
return None
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def sugerir_outliers(df_metricas, coluna, valor):
|
| 492 |
+
"""
|
| 493 |
+
Aplica filtro simétrico e retorna lista de índices sugeridos como outliers.
|
| 494 |
+
Filtra: coluna <= -valor OU coluna >= +valor
|
| 495 |
+
|
| 496 |
+
Parâmetros:
|
| 497 |
+
df_metricas: DataFrame com métricas de outliers
|
| 498 |
+
coluna: nome da coluna para filtro (ex: "Resíduo Pad.")
|
| 499 |
+
valor: valor absoluto do limite (ex: 2.0 significa <= -2 OU >= +2)
|
| 500 |
+
|
| 501 |
+
Retorna:
|
| 502 |
+
Lista de índices sugeridos como outliers
|
| 503 |
+
"""
|
| 504 |
+
if df_metricas is None or df_metricas.empty:
|
| 505 |
+
return []
|
| 506 |
+
|
| 507 |
+
if coluna not in df_metricas.columns:
|
| 508 |
+
return []
|
| 509 |
+
|
| 510 |
+
# Filtro simétrico: <= -valor OU >= +valor
|
| 511 |
+
mask = (df_metricas[coluna] <= -valor) | (df_metricas[coluna] >= valor)
|
| 512 |
+
indices = df_metricas[mask].index.tolist()
|
| 513 |
+
|
| 514 |
+
return sorted(indices)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
# ============================================================
|
| 518 |
+
# TRANSFORMAÇÕES
|
| 519 |
+
# ============================================================
|
| 520 |
+
|
| 521 |
+
def aplicar_transformacao(data, transformacao):
|
| 522 |
+
"""
|
| 523 |
+
Aplica uma transformação matemática aos dados.
|
| 524 |
+
"""
|
| 525 |
+
data = np.asarray(data, dtype=float)
|
| 526 |
+
|
| 527 |
+
# Evita overflow em exp
|
| 528 |
+
if transformacao == "exp(x)" and (data > 50).any():
|
| 529 |
+
return data
|
| 530 |
+
|
| 531 |
+
if transformacao == "(x)":
|
| 532 |
+
return data
|
| 533 |
+
elif transformacao == "1/(x)":
|
| 534 |
+
return 1 / (data + 0.001)
|
| 535 |
+
elif transformacao == "ln(x)":
|
| 536 |
+
return np.log(np.maximum(data, 0.001))
|
| 537 |
+
elif transformacao == "exp(x)":
|
| 538 |
+
return np.exp(data)
|
| 539 |
+
elif transformacao == "(x)^2":
|
| 540 |
+
return data ** 2
|
| 541 |
+
elif transformacao == "raiz(x)":
|
| 542 |
+
return np.sqrt(np.maximum(data, 0))
|
| 543 |
+
elif transformacao == "1/raiz(x)":
|
| 544 |
+
return 1 / (np.sqrt(np.maximum(data, 0)) + 0.001)
|
| 545 |
+
else:
|
| 546 |
+
return data
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def inverter_transformacao_y(data, transformacao):
|
| 550 |
+
"""
|
| 551 |
+
Inverte a transformação aplicada em y para obter valores na escala original.
|
| 552 |
+
"""
|
| 553 |
+
data = np.asarray(data, dtype=float)
|
| 554 |
+
|
| 555 |
+
if transformacao in ["(y)", "(x)", "direct"]:
|
| 556 |
+
return data
|
| 557 |
+
elif transformacao in ["ln(y)", "ln(x)"]:
|
| 558 |
+
return np.exp(data)
|
| 559 |
+
elif transformacao in ["1/(y)", "1/(x)"]:
|
| 560 |
+
return 1 / (data + 0.001)
|
| 561 |
+
elif transformacao in ["exp(y)", "exp(x)"]:
|
| 562 |
+
return np.log(np.maximum(data, 0.001))
|
| 563 |
+
elif transformacao in ["(y)^2", "(x)^2"]:
|
| 564 |
+
return np.sqrt(np.maximum(data, 0))
|
| 565 |
+
elif transformacao in ["raiz(y)", "raiz(x)"]:
|
| 566 |
+
return data ** 2
|
| 567 |
+
elif transformacao in ["1/raiz(y)", "1/raiz(x)"]:
|
| 568 |
+
return 1 / (data ** 2 + 0.001)
|
| 569 |
+
else:
|
| 570 |
+
return data
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def formatar_transformacao(transformacao, is_y=False):
|
| 574 |
+
"""
|
| 575 |
+
Formata a string da transformação para exibição.
|
| 576 |
+
"""
|
| 577 |
+
if transformacao == "(x)":
|
| 578 |
+
return "(y)" if is_y else "(x)"
|
| 579 |
+
elif transformacao == "1/(x)":
|
| 580 |
+
return "1/(y)" if is_y else "1/(x)"
|
| 581 |
+
elif transformacao == "ln(x)":
|
| 582 |
+
return "ln(y)" if is_y else "ln(x)"
|
| 583 |
+
elif transformacao == "exp(x)":
|
| 584 |
+
return "exp(y)" if is_y else "exp(x)"
|
| 585 |
+
elif transformacao == "(x)^2":
|
| 586 |
+
return "(y)^2" if is_y else "(x)^2"
|
| 587 |
+
elif transformacao == "raiz(x)":
|
| 588 |
+
return "raiz(y)" if is_y else "raiz(x)"
|
| 589 |
+
elif transformacao == "1/raiz(x)":
|
| 590 |
+
return "1/raiz(y)" if is_y else "1/raiz(x)"
|
| 591 |
+
else:
|
| 592 |
+
return transformacao
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def detectar_dicotomicas(df, colunas):
|
| 596 |
+
"""
|
| 597 |
+
Detecta quais colunas são dicotômicas (apenas 0 e 1).
|
| 598 |
+
Retorna lista de nomes de colunas dicotômicas.
|
| 599 |
+
"""
|
| 600 |
+
dicotomicas = []
|
| 601 |
+
for col in colunas:
|
| 602 |
+
valores = set(df[col].dropna().unique())
|
| 603 |
+
if valores.issubset({0, 1, 0.0, 1.0}):
|
| 604 |
+
dicotomicas.append(col)
|
| 605 |
+
return dicotomicas
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def detectar_codigo_alocado(df, colunas):
|
| 609 |
+
"""
|
| 610 |
+
Detecta variáveis de código alocado/ajustado.
|
| 611 |
+
Critérios: todos os valores são inteiros, ≥3 valores distintos, nenhum zero.
|
| 612 |
+
Exclui variáveis já detectadas como dicotômicas (0/1).
|
| 613 |
+
Retorna lista de nomes de colunas de código alocado.
|
| 614 |
+
"""
|
| 615 |
+
resultado = []
|
| 616 |
+
for col in colunas:
|
| 617 |
+
valores = df[col].dropna().unique()
|
| 618 |
+
valores_set = set(valores)
|
| 619 |
+
# Pelo menos 3 valores distintos
|
| 620 |
+
if len(valores_set) < 3:
|
| 621 |
+
continue
|
| 622 |
+
# Todos devem ser inteiros
|
| 623 |
+
try:
|
| 624 |
+
if not all(float(v) == int(float(v)) for v in valores):
|
| 625 |
+
continue
|
| 626 |
+
except (ValueError, TypeError):
|
| 627 |
+
continue
|
| 628 |
+
# Nenhum valor zero
|
| 629 |
+
if any(float(v) == 0 for v in valores):
|
| 630 |
+
continue
|
| 631 |
+
resultado.append(col)
|
| 632 |
+
return resultado
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
def detectar_percentuais(df, colunas):
|
| 636 |
+
"""
|
| 637 |
+
Detecta variáveis percentuais (todos valores entre 0 e 1, com decimais ou >2 distintos).
|
| 638 |
+
Exclui variáveis já detectadas como dicotômicas puras (só {0, 1}).
|
| 639 |
+
Retorna lista de nomes de colunas percentuais.
|
| 640 |
+
"""
|
| 641 |
+
resultado = []
|
| 642 |
+
for col in colunas:
|
| 643 |
+
valores = df[col].dropna().unique()
|
| 644 |
+
if len(valores) < 2:
|
| 645 |
+
continue
|
| 646 |
+
# Todos devem estar entre 0 e 1
|
| 647 |
+
try:
|
| 648 |
+
if not all(0 <= float(v) <= 1 for v in valores):
|
| 649 |
+
continue
|
| 650 |
+
except (ValueError, TypeError):
|
| 651 |
+
continue
|
| 652 |
+
# Não pode ser dicotômica pura (só {0,1})
|
| 653 |
+
if set(valores).issubset({0, 1, 0.0, 1.0}):
|
| 654 |
+
continue
|
| 655 |
+
resultado.append(col)
|
| 656 |
+
return resultado
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
# ============================================================
|
| 660 |
+
# VERIFICAÇÃO DE MULTICOLINEARIDADE
|
| 661 |
+
# ============================================================
|
| 662 |
+
|
| 663 |
+
def verificar_multicolinearidade(df, colunas_x):
|
| 664 |
+
"""Verifica multicolinearidade na matriz de regressoras (dados brutos, sem transformação).
|
| 665 |
+
|
| 666 |
+
Calcula o posto da matriz (X + intercepto) para detectar dependência linear exata
|
| 667 |
+
e o VIF de cada variável para detectar colinearidade forte (mas não perfeita).
|
| 668 |
+
|
| 669 |
+
Retorna dict com:
|
| 670 |
+
'perfeita': bool — posto deficiente (matriz singular)
|
| 671 |
+
'alta': bool — alguma variável com VIF > 10
|
| 672 |
+
'vif': dict {col: float} — VIF de cada variável (vazio se perfeita ou < 2 vars)
|
| 673 |
+
'vars_alta': list — variáveis com VIF > 10
|
| 674 |
+
'posto': int — posto efetivo da matriz aumentada
|
| 675 |
+
'ncolunas': int — número de colunas da matriz aumentada
|
| 676 |
+
"""
|
| 677 |
+
from statsmodels.stats.outliers_influence import variance_inflation_factor
|
| 678 |
+
|
| 679 |
+
n_vars = len(colunas_x)
|
| 680 |
+
vazio = {'perfeita': False, 'alta': False, 'vif': {}, 'vars_alta': [], 'posto': 0, 'ncolunas': 0}
|
| 681 |
+
|
| 682 |
+
if n_vars < 2:
|
| 683 |
+
return vazio
|
| 684 |
+
|
| 685 |
+
try:
|
| 686 |
+
X_df = df[list(colunas_x)].dropna()
|
| 687 |
+
if len(X_df) == 0:
|
| 688 |
+
return vazio
|
| 689 |
+
X = X_df.values.astype(float)
|
| 690 |
+
|
| 691 |
+
# Remove colunas com variância zero (constantes puras — capturadas como dependência com intercepto)
|
| 692 |
+
std = X.std(axis=0)
|
| 693 |
+
X = X[:, std > 0]
|
| 694 |
+
cols_validas = [c for c, s in zip(colunas_x, std) if s > 0]
|
| 695 |
+
|
| 696 |
+
if len(cols_validas) < 2:
|
| 697 |
+
return vazio
|
| 698 |
+
|
| 699 |
+
X_const = np.column_stack([np.ones(len(X)), X])
|
| 700 |
+
posto = int(np.linalg.matrix_rank(X_const))
|
| 701 |
+
ncolunas = X_const.shape[1]
|
| 702 |
+
|
| 703 |
+
if posto < ncolunas:
|
| 704 |
+
return {
|
| 705 |
+
'perfeita': True, 'alta': True, 'vif': {}, 'vars_alta': [],
|
| 706 |
+
'posto': posto, 'ncolunas': ncolunas,
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
# Calcula VIF apenas quando n > k (amostra suficiente)
|
| 710 |
+
if len(X_const) <= ncolunas:
|
| 711 |
+
return vazio
|
| 712 |
+
|
| 713 |
+
vif = {}
|
| 714 |
+
for i, col in enumerate(cols_validas):
|
| 715 |
+
try:
|
| 716 |
+
v = float(variance_inflation_factor(X_const, i + 1)) # +1 pula intercepto
|
| 717 |
+
vif[col] = v
|
| 718 |
+
except Exception:
|
| 719 |
+
vif[col] = float('inf') # OLS interno singular → colinearidade exata para esta variável
|
| 720 |
+
|
| 721 |
+
# Verifica se algum VIF é infinito (colinearidade que o rank numérico não capturou)
|
| 722 |
+
if any(np.isinf(v) for v in vif.values()):
|
| 723 |
+
vars_inf = [c for c, v in vif.items() if np.isinf(v)]
|
| 724 |
+
vif_finito = {c: v for c, v in vif.items() if not np.isinf(v)}
|
| 725 |
+
return {
|
| 726 |
+
'perfeita': True, 'alta': True,
|
| 727 |
+
'vif': vif_finito, 'vars_alta': vars_inf,
|
| 728 |
+
'posto': posto, 'ncolunas': ncolunas,
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
vars_alta = [c for c, v in vif.items() if v > 10]
|
| 732 |
+
return {
|
| 733 |
+
'perfeita': False,
|
| 734 |
+
'alta': len(vars_alta) > 0,
|
| 735 |
+
'vif': vif,
|
| 736 |
+
'vars_alta': vars_alta,
|
| 737 |
+
'posto': posto,
|
| 738 |
+
'ncolunas': ncolunas,
|
| 739 |
+
}
|
| 740 |
+
except Exception:
|
| 741 |
+
return vazio
|
| 742 |
+
|
| 743 |
+
|
| 744 |
+
# ============================================================
|
| 745 |
+
# MODELO OLS E DIAGNÓSTICOS
|
| 746 |
+
# ============================================================
|
| 747 |
+
|
| 748 |
+
def ajustar_modelo(df, coluna_y, colunas_x, transformacao_y, transformacoes_x, indices_usar=None):
|
| 749 |
+
"""
|
| 750 |
+
Ajusta modelo OLS com as transformações especificadas.
|
| 751 |
+
|
| 752 |
+
Parâmetros:
|
| 753 |
+
df: DataFrame com os dados
|
| 754 |
+
coluna_y: nome da coluna da variável dependente
|
| 755 |
+
colunas_x: lista de nomes das colunas independentes
|
| 756 |
+
transformacao_y: transformação para y
|
| 757 |
+
transformacoes_x: dict {coluna: transformação} para X
|
| 758 |
+
indices_usar: lista de índices a incluir (se None, usa todos)
|
| 759 |
+
|
| 760 |
+
Retorna:
|
| 761 |
+
dict com resultados do modelo ou None se falhar
|
| 762 |
+
"""
|
| 763 |
+
if not colunas_x:
|
| 764 |
+
return None
|
| 765 |
+
|
| 766 |
+
# Filtra por índices se especificado
|
| 767 |
+
df_modelo = df.copy()
|
| 768 |
+
if indices_usar is not None:
|
| 769 |
+
df_modelo = df_modelo.loc[indices_usar]
|
| 770 |
+
|
| 771 |
+
# Prepara y transformado
|
| 772 |
+
y = df_modelo[coluna_y].values
|
| 773 |
+
y_transf = aplicar_transformacao(y, transformacao_y)
|
| 774 |
+
|
| 775 |
+
# Prepara X transformado
|
| 776 |
+
X_transf = pd.DataFrame(index=df_modelo.index)
|
| 777 |
+
for col in colunas_x:
|
| 778 |
+
transf = transformacoes_x.get(col, "(x)")
|
| 779 |
+
X_transf[col] = aplicar_transformacao(df_modelo[col].values, transf)
|
| 780 |
+
|
| 781 |
+
# Remove linhas com NaN ou Inf
|
| 782 |
+
mask = ~(np.isnan(y_transf) | np.isinf(y_transf))
|
| 783 |
+
for col in X_transf.columns:
|
| 784 |
+
mask &= ~(np.isnan(X_transf[col]) | np.isinf(X_transf[col]))
|
| 785 |
+
|
| 786 |
+
y_final = y_transf[mask]
|
| 787 |
+
X_final = X_transf.loc[mask.values if hasattr(mask, 'values') else mask]
|
| 788 |
+
indices_final = df_modelo.index[mask]
|
| 789 |
+
|
| 790 |
+
if len(y_final) < len(colunas_x) + 2:
|
| 791 |
+
return None
|
| 792 |
+
|
| 793 |
+
try:
|
| 794 |
+
# Ajusta modelo
|
| 795 |
+
X_sm = sm.add_constant(X_final)
|
| 796 |
+
modelo = sm.OLS(y_final, X_sm).fit()
|
| 797 |
+
|
| 798 |
+
# Calcula diagnósticos
|
| 799 |
+
diagnosticos = calcular_diagnosticos(modelo, X_final, y_final, coluna_y, transformacao_y, transformacoes_x)
|
| 800 |
+
|
| 801 |
+
# Tabela de coeficientes
|
| 802 |
+
tabela_coef = pd.DataFrame({
|
| 803 |
+
"Variável": modelo.params.index,
|
| 804 |
+
"Coeficiente": modelo.params.values,
|
| 805 |
+
"Erro Padrão": modelo.bse.values,
|
| 806 |
+
"t-Student": modelo.tvalues.values,
|
| 807 |
+
"p-valor": modelo.pvalues.values,
|
| 808 |
+
"Significância": [classificar_significancia(p) for p in modelo.pvalues.values]
|
| 809 |
+
})
|
| 810 |
+
|
| 811 |
+
# Tabela obs vs calc
|
| 812 |
+
y_obs = y_final
|
| 813 |
+
y_calc = modelo.predict()
|
| 814 |
+
residuos = modelo.resid
|
| 815 |
+
residuos_pad = residuos / diagnosticos["desvio_padrao_residuos"]
|
| 816 |
+
|
| 817 |
+
influence = OLSInfluence(modelo)
|
| 818 |
+
residuos_stud = influence.resid_studentized
|
| 819 |
+
cook = influence.cooks_distance[0]
|
| 820 |
+
|
| 821 |
+
tabela_obs_calc = pd.DataFrame({
|
| 822 |
+
"Índice": indices_final,
|
| 823 |
+
"Observado": y_obs,
|
| 824 |
+
"Calculado": y_calc,
|
| 825 |
+
"Resíduo": residuos,
|
| 826 |
+
"Resíduo Pad.": residuos_pad,
|
| 827 |
+
"Resíduo Stud.": residuos_stud,
|
| 828 |
+
"Cook": cook
|
| 829 |
+
})
|
| 830 |
+
|
| 831 |
+
# Dados transformados
|
| 832 |
+
X_esc_info = [f"{col}: {formatar_transformacao(transformacoes_x.get(col, '(x)'))}" for col in colunas_x]
|
| 833 |
+
y_esc_info = f"{coluna_y}: {formatar_transformacao(transformacao_y, is_y=True)}"
|
| 834 |
+
|
| 835 |
+
return {
|
| 836 |
+
"modelo_sm": modelo,
|
| 837 |
+
"diagnosticos": diagnosticos,
|
| 838 |
+
"tabela_coef": tabela_coef,
|
| 839 |
+
"tabela_obs_calc": tabela_obs_calc,
|
| 840 |
+
"X_transformado": X_final,
|
| 841 |
+
"y_transformado": pd.Series(y_final, index=indices_final, name=coluna_y),
|
| 842 |
+
"transformacoes_x": transformacoes_x.copy(),
|
| 843 |
+
"transformacao_y": transformacao_y,
|
| 844 |
+
"colunas_x": colunas_x.copy(),
|
| 845 |
+
"coluna_y": coluna_y,
|
| 846 |
+
"X_esc_info": X_esc_info,
|
| 847 |
+
"y_esc_info": y_esc_info,
|
| 848 |
+
"indices_usados": indices_final.tolist()
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
except Exception as e:
|
| 852 |
+
print(f"Erro ao ajustar modelo: {e}")
|
| 853 |
+
return None
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
def calcular_diagnosticos(modelo, X, y, coluna_y, transformacao_y, transformacoes_x):
|
| 857 |
+
"""
|
| 858 |
+
Calcula todos os diagnósticos do modelo.
|
| 859 |
+
"""
|
| 860 |
+
n = len(y)
|
| 861 |
+
k = X.shape[1]
|
| 862 |
+
residuos = modelo.resid
|
| 863 |
+
|
| 864 |
+
desvio_padrao_residuos = np.sqrt(np.sum(residuos**2) / (n - k - 1))
|
| 865 |
+
residuos_pad = residuos / desvio_padrao_residuos
|
| 866 |
+
|
| 867 |
+
# Métricas básicas
|
| 868 |
+
mse = np.mean(residuos**2)
|
| 869 |
+
r2 = modelo.rsquared
|
| 870 |
+
r2_ajustado = modelo.rsquared_adj
|
| 871 |
+
r_pearson = np.corrcoef(y, modelo.predict())[0, 1]
|
| 872 |
+
Fc = modelo.fvalue
|
| 873 |
+
p_valor_F = modelo.f_pvalue
|
| 874 |
+
|
| 875 |
+
# Interpretação F
|
| 876 |
+
if p_valor_F < 0.01:
|
| 877 |
+
interp_F = "✅ Fc > Ft: Significante ao nível de 1% (α = 1%) — Grau III"
|
| 878 |
+
elif p_valor_F < 0.02:
|
| 879 |
+
interp_F = "✅ Fc > Ft: Significante ao nível de 2% (α = 2%) — Grau II"
|
| 880 |
+
elif p_valor_F < 0.05:
|
| 881 |
+
interp_F = "✅ Fc > Ft: Significante ao nível de 5% (α = 5%) — Grau I"
|
| 882 |
+
else:
|
| 883 |
+
interp_F = "❌ Fc < Ft: Não significante ao nível de 5%"
|
| 884 |
+
|
| 885 |
+
# Teste KS (normalidade)
|
| 886 |
+
ks_stat, ks_p = kstest(residuos, "norm", args=(np.mean(residuos), np.std(residuos, ddof=1)))
|
| 887 |
+
if ks_p >= 0.05:
|
| 888 |
+
interp_KS = "✅ Não foi identificada evidência estatística de violação da normalidade dos resíduos (α = 5%)"
|
| 889 |
+
elif ks_p >= 0.02:
|
| 890 |
+
interp_KS = "❌ Rejeita-se a hipótese de normalidade dos resíduos (α = 5%)"
|
| 891 |
+
elif ks_p >= 0.01:
|
| 892 |
+
interp_KS = "❌ Rejeita-se a hipótese de normalidade dos resíduos (α = 2%)"
|
| 893 |
+
else:
|
| 894 |
+
interp_KS = "❌ Rejeita-se a hipótese de normalidade dos resíduos (α = 1%)"
|
| 895 |
+
|
| 896 |
+
# Percentuais da curva normal (formato compatível com .dai)
|
| 897 |
+
intervalos = [(-1.00, 1.00), (-1.64, 1.64), (-1.96, 1.96)]
|
| 898 |
+
percentuais = []
|
| 899 |
+
for min_int, max_int in intervalos:
|
| 900 |
+
count = np.sum((residuos_pad >= min_int) & (residuos_pad <= max_int))
|
| 901 |
+
perc = round(count / len(residuos) * 100, 0)
|
| 902 |
+
percentuais.append(f"{perc:.0f}%")
|
| 903 |
+
perc_resid = ", ".join(percentuais)
|
| 904 |
+
|
| 905 |
+
# Durbin-Watson
|
| 906 |
+
dw = durbin_watson(residuos)
|
| 907 |
+
k_dw = min(max(1, k), 4)
|
| 908 |
+
|
| 909 |
+
# Limites críticos a 5% (referência padrão)
|
| 910 |
+
tab5 = DW_TABLE[0.05][k_dw]
|
| 911 |
+
dL = np.interp(n, tab5["n"], tab5["dL"])
|
| 912 |
+
dU = np.interp(n, tab5["n"], tab5["dU"])
|
| 913 |
+
|
| 914 |
+
# Limites críticos a 1% (para escalonamento)
|
| 915 |
+
tab1 = DW_TABLE[0.01][k_dw]
|
| 916 |
+
dL_01 = np.interp(n, tab1["n"], tab1["dL"])
|
| 917 |
+
|
| 918 |
+
if dw < dL:
|
| 919 |
+
if dw < dL_01:
|
| 920 |
+
interp_DW = f"❌ Autocorrelação positiva (DW < dL = {dL_01:.4f}, α = 1%)"
|
| 921 |
+
else:
|
| 922 |
+
interp_DW = f"❌ Autocorrelação positiva (DW < dL = {dL:.4f}, α = 5%)"
|
| 923 |
+
elif dw > 4 - dL:
|
| 924 |
+
if dw > 4 - dL_01:
|
| 925 |
+
interp_DW = f"❌ Autocorrelação negativa (DW > 4−dL = {4-dL_01:.4f}, α = 1%)"
|
| 926 |
+
else:
|
| 927 |
+
interp_DW = f"❌ Autocorrelação negativa (DW > 4−dL = {4-dL:.4f}, α = 5%)"
|
| 928 |
+
elif dU < dw < 4 - dU:
|
| 929 |
+
interp_DW = f"✅ Não foi identificada evidência estatística de autocorrelação dos resíduos (dU = {dU:.4f} < DW < 4−dU = {4-dU:.4f})"
|
| 930 |
+
else:
|
| 931 |
+
interp_DW = f"⚠️ Região inconclusiva (dL = {dL:.4f}, dU = {dU:.4f})"
|
| 932 |
+
|
| 933 |
+
# Breusch-Pagan
|
| 934 |
+
bp_lm, bp_p, bp_f, bp_fp = het_breuschpagan(residuos, sm.add_constant(X))
|
| 935 |
+
if bp_p >= 0.05:
|
| 936 |
+
interp_BP = "✅ Não foi identificada evidência estatística de heterocedasticidade (α = 5%)"
|
| 937 |
+
elif bp_p >= 0.02:
|
| 938 |
+
interp_BP = "❌ Rejeita-se a hipótese de homocedasticidade (α = 5%)"
|
| 939 |
+
elif bp_p >= 0.01:
|
| 940 |
+
interp_BP = "❌ Rejeita-se a hipótese de homocedasticidade (α = 2%)"
|
| 941 |
+
else:
|
| 942 |
+
interp_BP = "❌ Rejeita-se a hipótese de homocedasticidade (α = 1%)"
|
| 943 |
+
|
| 944 |
+
# Equação do modelo
|
| 945 |
+
equacao = formatar_equacao(modelo, coluna_y, transformacao_y, transformacoes_x, list(X.columns))
|
| 946 |
+
|
| 947 |
+
# Gera formatted_output para compatibilidade com .dai
|
| 948 |
+
formatted_output = f"""Número de observações: {n}
|
| 949 |
+
Número de variáveis independentes: {k}
|
| 950 |
+
Desvio padrão dos resíduos: {desvio_padrao_residuos:.4f}
|
| 951 |
+
MSE: {mse:.4f}
|
| 952 |
+
R²: {r2:.4f}
|
| 953 |
+
R² ajustado: {r2_ajustado:.4f}
|
| 954 |
+
Correlação Pearson: {r_pearson:.4f}
|
| 955 |
+
Estatística F: {Fc:.4f} (p-valor: {p_valor_F:.4f}) -> {interp_F}
|
| 956 |
+
----------------------------------------------------------------------
|
| 957 |
+
Teste de Normalidade (Kolmogorov-Smirnov):
|
| 958 |
+
Estatística KS: {ks_stat:.4f}, p-valor: {ks_p:.4f} -> {interp_KS}
|
| 959 |
+
Teste de Normalidade (Comparação com a curva normal) – percentuais atingidos: {perc_resid}
|
| 960 |
+
• Ideal 68% → aceitável entre 64% e 75%
|
| 961 |
+
• Ideal 90% → aceitável entre 88% e 95%
|
| 962 |
+
• Ideal 95% → aceitável entre 95% e 100%
|
| 963 |
+
----------------------------------------------------------------------
|
| 964 |
+
Teste de Autocorrelação (Durbin-Watson):
|
| 965 |
+
DW: {dw:.4f} (dL = {dL:.4f}, dU = {dU:.4f}) -> {interp_DW}
|
| 966 |
+
----------------------------------------------------------------------
|
| 967 |
+
Teste de Homocedasticidade (Breusch-Pagan):
|
| 968 |
+
Estatística LM: {bp_lm:.4f}, p-valor: {bp_p:.4f}
|
| 969 |
+
{interp_BP}
|
| 970 |
+
----------------------------------------------------------------------
|
| 971 |
+
|
| 972 |
+
Equação do Modelo:
|
| 973 |
+
{equacao}"""
|
| 974 |
+
|
| 975 |
+
return {
|
| 976 |
+
"n": n,
|
| 977 |
+
"k": k,
|
| 978 |
+
"desvio_padrao_residuos": desvio_padrao_residuos,
|
| 979 |
+
"mse": mse,
|
| 980 |
+
"r2": r2,
|
| 981 |
+
"r2_ajustado": r2_ajustado,
|
| 982 |
+
"r_pearson": r_pearson,
|
| 983 |
+
"Fc": Fc,
|
| 984 |
+
"p_valor_F": p_valor_F,
|
| 985 |
+
"interp_F": interp_F,
|
| 986 |
+
"ks_stat": ks_stat,
|
| 987 |
+
"ks_p": ks_p,
|
| 988 |
+
"interp_KS": interp_KS,
|
| 989 |
+
"perc_resid": perc_resid,
|
| 990 |
+
"dw": dw,
|
| 991 |
+
"interp_DW": interp_DW,
|
| 992 |
+
"bp_lm": bp_lm,
|
| 993 |
+
"bp_p": bp_p,
|
| 994 |
+
"interp_BP": interp_BP,
|
| 995 |
+
"equacao": equacao,
|
| 996 |
+
"formatted_output": formatted_output
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
def classificar_significancia(p_valor):
|
| 1001 |
+
"""Classifica significância segundo NBR 14.653-2."""
|
| 1002 |
+
if p_valor <= 0.10:
|
| 1003 |
+
return "Grau III"
|
| 1004 |
+
elif p_valor <= 0.20:
|
| 1005 |
+
return "Grau II"
|
| 1006 |
+
elif p_valor <= 0.30:
|
| 1007 |
+
return "Grau I"
|
| 1008 |
+
else:
|
| 1009 |
+
return "Sem enquadramento"
|
| 1010 |
+
|
| 1011 |
+
|
| 1012 |
+
def formatar_equacao(modelo, coluna_y, transformacao_y, transformacoes_x, colunas_x):
|
| 1013 |
+
"""Formata a equação do modelo."""
|
| 1014 |
+
equacao = f"{modelo.params['const']:.6f}"
|
| 1015 |
+
|
| 1016 |
+
for col in colunas_x:
|
| 1017 |
+
coef = modelo.params[col]
|
| 1018 |
+
transf = transformacoes_x.get(col, "(x)")
|
| 1019 |
+
|
| 1020 |
+
if transf == "ln(x)":
|
| 1021 |
+
termo = f"ln({col})"
|
| 1022 |
+
elif transf == "1/(x)":
|
| 1023 |
+
termo = f"1/{col}"
|
| 1024 |
+
elif transf == "(x)^2":
|
| 1025 |
+
termo = f"({col})²"
|
| 1026 |
+
elif transf == "raiz(x)":
|
| 1027 |
+
termo = f"√({col})"
|
| 1028 |
+
elif transf == "1/raiz(x)":
|
| 1029 |
+
termo = f"1/√({col})"
|
| 1030 |
+
elif transf == "exp(x)":
|
| 1031 |
+
termo = f"exp({col})"
|
| 1032 |
+
else:
|
| 1033 |
+
termo = col
|
| 1034 |
+
|
| 1035 |
+
sinal = "+" if coef >= 0 else "-"
|
| 1036 |
+
equacao += f" {sinal} {abs(coef):.6f} × {termo}"
|
| 1037 |
+
|
| 1038 |
+
# Aplica inversão de y
|
| 1039 |
+
if transformacao_y == "ln(x)":
|
| 1040 |
+
equacao = f"exp({equacao})"
|
| 1041 |
+
elif transformacao_y == "(x)^2":
|
| 1042 |
+
equacao = f"√({equacao})"
|
| 1043 |
+
elif transformacao_y == "1/(x)":
|
| 1044 |
+
equacao = f"1/({equacao})"
|
| 1045 |
+
elif transformacao_y == "raiz(x)":
|
| 1046 |
+
equacao = f"({equacao})²"
|
| 1047 |
+
elif transformacao_y == "1/raiz(x)":
|
| 1048 |
+
equacao = f"1/({equacao})²"
|
| 1049 |
+
|
| 1050 |
+
return f"{coluna_y} = {equacao}"
|
| 1051 |
+
|
| 1052 |
+
|
| 1053 |
+
# ============================================================
|
| 1054 |
+
# BUSCA AUTOMÁTICA DE TRANSFORMAÇÕES (OTIMIZADA)
|
| 1055 |
+
# ============================================================
|
| 1056 |
+
|
| 1057 |
+
def _calcular_r2_numpy(X, y):
|
| 1058 |
+
"""
|
| 1059 |
+
Calcula R² usando operações matriciais diretas (mais rápido que statsmodels).
|
| 1060 |
+
"""
|
| 1061 |
+
n = len(y)
|
| 1062 |
+
if n == 0:
|
| 1063 |
+
return -np.inf
|
| 1064 |
+
|
| 1065 |
+
# Adiciona constante
|
| 1066 |
+
X_const = np.column_stack([np.ones(n), X])
|
| 1067 |
+
|
| 1068 |
+
try:
|
| 1069 |
+
# OLS: beta = (X'X)^-1 X'y
|
| 1070 |
+
XtX = X_const.T @ X_const
|
| 1071 |
+
Xty = X_const.T @ y
|
| 1072 |
+
beta = np.linalg.solve(XtX, Xty)
|
| 1073 |
+
|
| 1074 |
+
# R²
|
| 1075 |
+
y_pred = X_const @ beta
|
| 1076 |
+
ss_res = np.sum((y - y_pred) ** 2)
|
| 1077 |
+
ss_tot = np.sum((y - np.mean(y)) ** 2)
|
| 1078 |
+
|
| 1079 |
+
if ss_tot == 0:
|
| 1080 |
+
return -np.inf
|
| 1081 |
+
|
| 1082 |
+
return 1 - (ss_res / ss_tot)
|
| 1083 |
+
except:
|
| 1084 |
+
return -np.inf
|
| 1085 |
+
|
| 1086 |
+
|
| 1087 |
+
def _calcular_pvalores_numpy(X, y):
|
| 1088 |
+
"""
|
| 1089 |
+
Calcula p-valores dos coeficientes X e p-valor do teste F usando operações matriciais.
|
| 1090 |
+
Retorna (p_valores_coeficientes, p_valor_f) ou (None, None) se falhar.
|
| 1091 |
+
p_valores_coeficientes inclui a constante (intercepto) no índice 0.
|
| 1092 |
+
"""
|
| 1093 |
+
from scipy.stats import t as t_dist, f as f_dist
|
| 1094 |
+
n = len(y)
|
| 1095 |
+
X_const = np.column_stack([np.ones(n), X])
|
| 1096 |
+
k = X_const.shape[1] # inclui constante
|
| 1097 |
+
if n <= k:
|
| 1098 |
+
return None, None
|
| 1099 |
+
try:
|
| 1100 |
+
XtX = X_const.T @ X_const
|
| 1101 |
+
XtX_inv = np.linalg.inv(XtX)
|
| 1102 |
+
beta = XtX_inv @ (X_const.T @ y)
|
| 1103 |
+
y_pred = X_const @ beta
|
| 1104 |
+
residuals = y - y_pred
|
| 1105 |
+
ss_res = np.sum(residuals ** 2)
|
| 1106 |
+
sigma2 = ss_res / (n - k)
|
| 1107 |
+
if sigma2 <= 0:
|
| 1108 |
+
return None, None
|
| 1109 |
+
diag_vals = np.diag(sigma2 * XtX_inv)
|
| 1110 |
+
if np.any(diag_vals < 0):
|
| 1111 |
+
return None, None # matriz mal-condicionada (multicolinearidade)
|
| 1112 |
+
se = np.sqrt(diag_vals)
|
| 1113 |
+
if np.any(se == 0):
|
| 1114 |
+
return None, None
|
| 1115 |
+
t_stats = beta / se
|
| 1116 |
+
p_valores = 2 * t_dist.sf(np.abs(t_stats), df=n - k)
|
| 1117 |
+
|
| 1118 |
+
# Teste F: (SS_reg / (k-1)) / (SS_res / (n-k))
|
| 1119 |
+
y_mean = np.mean(y)
|
| 1120 |
+
ss_tot = np.sum((y - y_mean) ** 2)
|
| 1121 |
+
ss_reg = ss_tot - ss_res
|
| 1122 |
+
df_reg = k - 1 # graus de liberdade da regressão (exclui constante)
|
| 1123 |
+
df_res = n - k
|
| 1124 |
+
if df_reg > 0 and df_res > 0 and ss_res > 0:
|
| 1125 |
+
f_stat = (ss_reg / df_reg) / (ss_res / df_res)
|
| 1126 |
+
p_valor_f = f_dist.sf(f_stat, df_reg, df_res)
|
| 1127 |
+
else:
|
| 1128 |
+
p_valor_f = 1.0
|
| 1129 |
+
|
| 1130 |
+
return p_valores, p_valor_f # inclui constante (intercepto) no índice 0
|
| 1131 |
+
except:
|
| 1132 |
+
return None, None
|
| 1133 |
+
|
| 1134 |
+
|
| 1135 |
+
def _precomputar_transformacoes(df, colunas):
|
| 1136 |
+
"""
|
| 1137 |
+
Pré-computa todas as transformações válidas para as colunas.
|
| 1138 |
+
Retorna dict: {(coluna, transformacao): array}
|
| 1139 |
+
"""
|
| 1140 |
+
cache = {}
|
| 1141 |
+
|
| 1142 |
+
for col in colunas:
|
| 1143 |
+
valores = df[col].values.astype(float)
|
| 1144 |
+
for transf in TRANSFORMACOES:
|
| 1145 |
+
dados_transf = aplicar_transformacao(valores, transf)
|
| 1146 |
+
|
| 1147 |
+
# Só armazena se válido
|
| 1148 |
+
if not (np.isnan(dados_transf).any() or np.isinf(dados_transf).any()):
|
| 1149 |
+
cache[(col, transf)] = dados_transf
|
| 1150 |
+
|
| 1151 |
+
return cache
|
| 1152 |
+
|
| 1153 |
+
|
| 1154 |
+
def _buscar_stepwise(colunas_x, colunas_livres, transformacoes_fixas, y_transf, x_cache, df_busca):
|
| 1155 |
+
"""
|
| 1156 |
+
Busca stepwise: otimiza uma variável por vez.
|
| 1157 |
+
Mais rápido para muitas variáveis (O(7*n) em vez de O(7^n)).
|
| 1158 |
+
"""
|
| 1159 |
+
# Inicializa com transformação (x) para todas
|
| 1160 |
+
transf_x_atuais = {col: "(x)" for col in colunas_livres}
|
| 1161 |
+
|
| 1162 |
+
# Para cada variável X, encontrar melhor transformação
|
| 1163 |
+
for col in colunas_livres:
|
| 1164 |
+
melhor_transf = "(x)"
|
| 1165 |
+
melhor_r2 = -np.inf
|
| 1166 |
+
|
| 1167 |
+
for transf in TRANSFORMACOES:
|
| 1168 |
+
if (col, transf) not in x_cache:
|
| 1169 |
+
continue
|
| 1170 |
+
|
| 1171 |
+
# Testa com esta transformação
|
| 1172 |
+
transf_teste = transf_x_atuais.copy()
|
| 1173 |
+
transf_teste[col] = transf
|
| 1174 |
+
|
| 1175 |
+
# Monta matriz X
|
| 1176 |
+
X_arrays = []
|
| 1177 |
+
valido = True
|
| 1178 |
+
for c in colunas_x:
|
| 1179 |
+
if c in transformacoes_fixas:
|
| 1180 |
+
t = transformacoes_fixas[c]
|
| 1181 |
+
else:
|
| 1182 |
+
t = transf_teste.get(c, "(x)")
|
| 1183 |
+
|
| 1184 |
+
if (c, t) in x_cache:
|
| 1185 |
+
X_arrays.append(x_cache[(c, t)])
|
| 1186 |
+
else:
|
| 1187 |
+
valido = False
|
| 1188 |
+
break
|
| 1189 |
+
|
| 1190 |
+
if not valido or not X_arrays:
|
| 1191 |
+
continue
|
| 1192 |
+
|
| 1193 |
+
X = np.column_stack(X_arrays)
|
| 1194 |
+
r2 = _calcular_r2_numpy(X, y_transf)
|
| 1195 |
+
|
| 1196 |
+
if r2 > melhor_r2:
|
| 1197 |
+
melhor_r2 = r2
|
| 1198 |
+
melhor_transf = transf
|
| 1199 |
+
|
| 1200 |
+
transf_x_atuais[col] = melhor_transf
|
| 1201 |
+
|
| 1202 |
+
# Calcula R² final
|
| 1203 |
+
transf_x_final = {**transformacoes_fixas, **transf_x_atuais}
|
| 1204 |
+
X_arrays = []
|
| 1205 |
+
for c in colunas_x:
|
| 1206 |
+
t = transf_x_final.get(c, "(x)")
|
| 1207 |
+
if (c, t) in x_cache:
|
| 1208 |
+
X_arrays.append(x_cache[(c, t)])
|
| 1209 |
+
|
| 1210 |
+
if X_arrays:
|
| 1211 |
+
X = np.column_stack(X_arrays)
|
| 1212 |
+
r2_final = _calcular_r2_numpy(X, y_transf)
|
| 1213 |
+
else:
|
| 1214 |
+
r2_final = -np.inf
|
| 1215 |
+
|
| 1216 |
+
return transf_x_final, r2_final
|
| 1217 |
+
|
| 1218 |
+
|
| 1219 |
+
def _buscar_exaustivo_numpy(colunas_x, colunas_livres, transformacoes_fixas, y_transf, x_cache, top_n):
|
| 1220 |
+
"""
|
| 1221 |
+
Busca exaustiva usando numpy para cálculo rápido de R².
|
| 1222 |
+
Para uso quando o número de combinações é pequeno (<= 5000).
|
| 1223 |
+
"""
|
| 1224 |
+
n_livres = len(colunas_livres)
|
| 1225 |
+
resultados_transf = []
|
| 1226 |
+
|
| 1227 |
+
for combo in product(TRANSFORMACOES, repeat=n_livres):
|
| 1228 |
+
# Monta dict de transformações
|
| 1229 |
+
transf_x = transformacoes_fixas.copy()
|
| 1230 |
+
for i, col in enumerate(colunas_livres):
|
| 1231 |
+
transf_x[col] = combo[i]
|
| 1232 |
+
|
| 1233 |
+
# Monta matriz X
|
| 1234 |
+
X_arrays = []
|
| 1235 |
+
valido = True
|
| 1236 |
+
for col in colunas_x:
|
| 1237 |
+
t = transf_x.get(col, "(x)")
|
| 1238 |
+
if (col, t) in x_cache:
|
| 1239 |
+
X_arrays.append(x_cache[(col, t)])
|
| 1240 |
+
else:
|
| 1241 |
+
valido = False
|
| 1242 |
+
break
|
| 1243 |
+
|
| 1244 |
+
if not valido or not X_arrays:
|
| 1245 |
+
continue
|
| 1246 |
+
|
| 1247 |
+
X = np.column_stack(X_arrays)
|
| 1248 |
+
r2 = _calcular_r2_numpy(X, y_transf)
|
| 1249 |
+
|
| 1250 |
+
if r2 > -np.inf:
|
| 1251 |
+
resultados_transf.append((r2, transf_x.copy()))
|
| 1252 |
+
|
| 1253 |
+
# Ordena e retorna top
|
| 1254 |
+
resultados_transf.sort(key=lambda x: x[0], reverse=True)
|
| 1255 |
+
return resultados_transf[:top_n]
|
| 1256 |
+
|
| 1257 |
+
|
| 1258 |
+
def _classificar_grau_coef(p):
|
| 1259 |
+
"""Classifica p-valor de coeficiente em grau de enquadramento (0-3)."""
|
| 1260 |
+
if p is None or p > 0.30:
|
| 1261 |
+
return 0
|
| 1262 |
+
if p > 0.20:
|
| 1263 |
+
return 1
|
| 1264 |
+
if p > 0.10:
|
| 1265 |
+
return 2
|
| 1266 |
+
return 3
|
| 1267 |
+
|
| 1268 |
+
|
| 1269 |
+
def _classificar_grau_f(p):
|
| 1270 |
+
"""Classifica p-valor do teste F em grau de enquadramento (0-3)."""
|
| 1271 |
+
if p is None or p > 0.05:
|
| 1272 |
+
return 0
|
| 1273 |
+
if p > 0.02:
|
| 1274 |
+
return 1
|
| 1275 |
+
if p > 0.01:
|
| 1276 |
+
return 2
|
| 1277 |
+
return 3
|
| 1278 |
+
|
| 1279 |
+
|
| 1280 |
+
def buscar_melhores_transformacoes(df, coluna_y, colunas_x, transformacoes_fixas=None, transformacao_y_fixa=None,
|
| 1281 |
+
indices_usar=None, top_n=5,
|
| 1282 |
+
grau_min_coef=3, grau_min_f=3):
|
| 1283 |
+
"""
|
| 1284 |
+
Busca otimizada das melhores combinações de transformações para maximizar R².
|
| 1285 |
+
|
| 1286 |
+
Usa estratégia híbrida:
|
| 1287 |
+
- Para poucos combos (<=5000): busca exaustiva com numpy (rápida)
|
| 1288 |
+
- Para muitos combos (>5000): busca stepwise (O(7*n) em vez de O(7^n))
|
| 1289 |
+
|
| 1290 |
+
Parâmetros:
|
| 1291 |
+
df: DataFrame
|
| 1292 |
+
coluna_y: variável dependente
|
| 1293 |
+
colunas_x: lista de variáveis independentes
|
| 1294 |
+
transformacoes_fixas: dict {coluna: transformação} para variáveis com transformação fixa
|
| 1295 |
+
transformacao_y_fixa: transformação fixa para y (ou None para testar todas)
|
| 1296 |
+
indices_usar: lista de índices a incluir (se None, usa todos)
|
| 1297 |
+
top_n: número de melhores modelos a retornar
|
| 1298 |
+
grau_min_coef: grau mínimo de significância dos coeficientes (3=Grau III p≤10%, 2=Grau II p≤20%, 1=Grau I p≤30%, 0=sem filtro)
|
| 1299 |
+
grau_min_f: grau mínimo do teste F (3=Grau III α=1%, 2=Grau II α=2%, 1=Grau I α=5%, 0=sem filtro)
|
| 1300 |
+
|
| 1301 |
+
Retorna:
|
| 1302 |
+
Lista de dicts com informações dos top N modelos
|
| 1303 |
+
"""
|
| 1304 |
+
if transformacoes_fixas is None:
|
| 1305 |
+
transformacoes_fixas = {}
|
| 1306 |
+
|
| 1307 |
+
# Filtra por índices se especificado
|
| 1308 |
+
df_busca = df.copy()
|
| 1309 |
+
if indices_usar is not None:
|
| 1310 |
+
df_busca = df_busca.loc[indices_usar]
|
| 1311 |
+
|
| 1312 |
+
# Detecta dicotômicas e percentuais e fixa em (x) (código alocado fica livre)
|
| 1313 |
+
dicotomicas = detectar_dicotomicas(df_busca, colunas_x)
|
| 1314 |
+
percentuais = detectar_percentuais(df_busca, colunas_x)
|
| 1315 |
+
for col in dicotomicas + percentuais:
|
| 1316 |
+
if col not in transformacoes_fixas:
|
| 1317 |
+
transformacoes_fixas[col] = "(x)"
|
| 1318 |
+
|
| 1319 |
+
# Identifica colunas livres (não fixas)
|
| 1320 |
+
colunas_livres = [col for col in colunas_x if col not in transformacoes_fixas]
|
| 1321 |
+
n_livres = len(colunas_livres)
|
| 1322 |
+
|
| 1323 |
+
# Define transformações de Y a testar
|
| 1324 |
+
if transformacao_y_fixa:
|
| 1325 |
+
transformacoes_y = [transformacao_y_fixa]
|
| 1326 |
+
else:
|
| 1327 |
+
transformacoes_y = TRANSFORMACOES
|
| 1328 |
+
|
| 1329 |
+
# Pré-computa todas as transformações de X (apenas colunas livres)
|
| 1330 |
+
x_cache = _precomputar_transformacoes(df_busca, colunas_livres)
|
| 1331 |
+
|
| 1332 |
+
# Adiciona transformações fixas ao cache
|
| 1333 |
+
for col, transf in transformacoes_fixas.items():
|
| 1334 |
+
if col in colunas_x:
|
| 1335 |
+
dados = aplicar_transformacao(df_busca[col].values.astype(float), transf)
|
| 1336 |
+
if not (np.isnan(dados).any() or np.isinf(dados).any()):
|
| 1337 |
+
x_cache[(col, transf)] = dados
|
| 1338 |
+
|
| 1339 |
+
# Calcula número total de combinações
|
| 1340 |
+
total_combos = len(TRANSFORMACOES) ** n_livres if n_livres > 0 else 1
|
| 1341 |
+
usar_stepwise = total_combos > 5000
|
| 1342 |
+
|
| 1343 |
+
# Mapeamento de grau para limiar de p-valor
|
| 1344 |
+
_LIMIARES_COEF = {3: 0.10, 2: 0.20, 1: 0.30}
|
| 1345 |
+
_LIMIARES_F = {3: 0.01, 2: 0.02, 1: 0.05}
|
| 1346 |
+
limiar_coef = _LIMIARES_COEF.get(grau_min_coef)
|
| 1347 |
+
limiar_f = _LIMIARES_F.get(grau_min_f)
|
| 1348 |
+
|
| 1349 |
+
# Coleta mais candidatos quando vai filtrar
|
| 1350 |
+
tem_filtro = limiar_coef is not None or limiar_f is not None
|
| 1351 |
+
top_n_interno = top_n * 10 if tem_filtro else top_n
|
| 1352 |
+
|
| 1353 |
+
resultados = []
|
| 1354 |
+
|
| 1355 |
+
for transf_y in transformacoes_y:
|
| 1356 |
+
# Pré-computa Y transformado
|
| 1357 |
+
y_transf = aplicar_transformacao(df_busca[coluna_y].values.astype(float), transf_y)
|
| 1358 |
+
|
| 1359 |
+
# Se y_transf tem problemas, pula
|
| 1360 |
+
if np.isnan(y_transf).any() or np.isinf(y_transf).any():
|
| 1361 |
+
continue
|
| 1362 |
+
|
| 1363 |
+
if n_livres == 0:
|
| 1364 |
+
# Todas as variáveis são fixas, só calcula R²
|
| 1365 |
+
X_arrays = [x_cache[(col, transformacoes_fixas[col])]
|
| 1366 |
+
for col in colunas_x if (col, transformacoes_fixas.get(col, "(x)")) in x_cache]
|
| 1367 |
+
if X_arrays:
|
| 1368 |
+
X = np.column_stack(X_arrays)
|
| 1369 |
+
r2 = _calcular_r2_numpy(X, y_transf)
|
| 1370 |
+
if r2 > -np.inf:
|
| 1371 |
+
resultados.append({
|
| 1372 |
+
"r2": r2,
|
| 1373 |
+
"transformacao_y": transf_y,
|
| 1374 |
+
"transformacoes_x": transformacoes_fixas.copy()
|
| 1375 |
+
})
|
| 1376 |
+
elif usar_stepwise:
|
| 1377 |
+
# Busca stepwise para muitas variáveis
|
| 1378 |
+
transf_x, r2 = _buscar_stepwise(
|
| 1379 |
+
colunas_x, colunas_livres, transformacoes_fixas,
|
| 1380 |
+
y_transf, x_cache, df_busca
|
| 1381 |
+
)
|
| 1382 |
+
if r2 > -np.inf:
|
| 1383 |
+
resultados.append({
|
| 1384 |
+
"r2": r2,
|
| 1385 |
+
"transformacao_y": transf_y,
|
| 1386 |
+
"transformacoes_x": transf_x
|
| 1387 |
+
})
|
| 1388 |
+
else:
|
| 1389 |
+
# Busca exaustiva para poucas variáveis
|
| 1390 |
+
top_combos = _buscar_exaustivo_numpy(
|
| 1391 |
+
colunas_x, colunas_livres, transformacoes_fixas,
|
| 1392 |
+
y_transf, x_cache, top_n_interno
|
| 1393 |
+
)
|
| 1394 |
+
for r2, transf_x in top_combos:
|
| 1395 |
+
resultados.append({
|
| 1396 |
+
"r2": r2,
|
| 1397 |
+
"transformacao_y": transf_y,
|
| 1398 |
+
"transformacoes_x": transf_x
|
| 1399 |
+
})
|
| 1400 |
+
|
| 1401 |
+
# Ordena por R²
|
| 1402 |
+
resultados = sorted(resultados, key=lambda x: x["r2"], reverse=True)
|
| 1403 |
+
|
| 1404 |
+
# Filtra por significância dos coeficientes e/ou teste F
|
| 1405 |
+
if tem_filtro and resultados:
|
| 1406 |
+
resultados_filtrados = []
|
| 1407 |
+
for r in resultados:
|
| 1408 |
+
transf_y = r["transformacao_y"]
|
| 1409 |
+
transf_x = r["transformacoes_x"]
|
| 1410 |
+
|
| 1411 |
+
# Monta Y transformado
|
| 1412 |
+
y_transf = aplicar_transformacao(df_busca[coluna_y].values.astype(float), transf_y)
|
| 1413 |
+
if np.isnan(y_transf).any() or np.isinf(y_transf).any():
|
| 1414 |
+
continue
|
| 1415 |
+
|
| 1416 |
+
# Monta X transformado
|
| 1417 |
+
X_arrays = []
|
| 1418 |
+
valido = True
|
| 1419 |
+
for col in colunas_x:
|
| 1420 |
+
t = transf_x.get(col, "(x)")
|
| 1421 |
+
if (col, t) in x_cache:
|
| 1422 |
+
X_arrays.append(x_cache[(col, t)])
|
| 1423 |
+
else:
|
| 1424 |
+
valido = False
|
| 1425 |
+
break
|
| 1426 |
+
|
| 1427 |
+
if not valido or not X_arrays:
|
| 1428 |
+
continue
|
| 1429 |
+
|
| 1430 |
+
X = np.column_stack(X_arrays)
|
| 1431 |
+
p_valores_coef, p_valor_f = _calcular_pvalores_numpy(X, y_transf)
|
| 1432 |
+
|
| 1433 |
+
# Filtro de significância dos coeficientes
|
| 1434 |
+
if limiar_coef is not None:
|
| 1435 |
+
if p_valores_coef is None or not all(p <= limiar_coef for p in p_valores_coef):
|
| 1436 |
+
continue
|
| 1437 |
+
|
| 1438 |
+
# Filtro do teste F
|
| 1439 |
+
if limiar_f is not None:
|
| 1440 |
+
if p_valor_f is None or p_valor_f >= limiar_f:
|
| 1441 |
+
continue
|
| 1442 |
+
|
| 1443 |
+
resultados_filtrados.append(r)
|
| 1444 |
+
|
| 1445 |
+
if len(resultados_filtrados) >= top_n:
|
| 1446 |
+
break
|
| 1447 |
+
|
| 1448 |
+
resultados = resultados_filtrados
|
| 1449 |
+
|
| 1450 |
+
resultados = resultados[:top_n]
|
| 1451 |
+
|
| 1452 |
+
# Adiciona ranking
|
| 1453 |
+
for i, r in enumerate(resultados):
|
| 1454 |
+
r["rank"] = i + 1
|
| 1455 |
+
|
| 1456 |
+
# Enriquece resultados com graus de enquadramento por variável e teste F
|
| 1457 |
+
for r in resultados:
|
| 1458 |
+
y_tc = aplicar_transformacao(df_busca[coluna_y].values.astype(float), r["transformacao_y"])
|
| 1459 |
+
X_arrs, valido = [], True
|
| 1460 |
+
for col in colunas_x:
|
| 1461 |
+
t = r["transformacoes_x"].get(col, "(x)")
|
| 1462 |
+
if (col, t) in x_cache:
|
| 1463 |
+
X_arrs.append(x_cache[(col, t)])
|
| 1464 |
+
else:
|
| 1465 |
+
valido = False
|
| 1466 |
+
break
|
| 1467 |
+
if valido and X_arrs and not (np.isnan(y_tc).any() or np.isinf(y_tc).any()):
|
| 1468 |
+
p_vals, p_f = _calcular_pvalores_numpy(np.column_stack(X_arrs), y_tc)
|
| 1469 |
+
if p_vals is not None:
|
| 1470 |
+
r["graus_coef"] = {col: _classificar_grau_coef(float(p_vals[i + 1]))
|
| 1471 |
+
for i, col in enumerate(colunas_x)}
|
| 1472 |
+
else:
|
| 1473 |
+
r["graus_coef"] = {col: 0 for col in colunas_x}
|
| 1474 |
+
r["grau_f"] = _classificar_grau_f(p_f) if p_f is not None else 0
|
| 1475 |
+
else:
|
| 1476 |
+
r["graus_coef"] = {col: 0 for col in colunas_x}
|
| 1477 |
+
r["grau_f"] = 0
|
| 1478 |
+
|
| 1479 |
+
return resultados
|
| 1480 |
+
|
| 1481 |
+
|
| 1482 |
+
# ============================================================
|
| 1483 |
+
# MICRONUMEROSIDADE (NBR 14.653-2)
|
| 1484 |
+
# ============================================================
|
| 1485 |
+
|
| 1486 |
+
def _testar_frequencia_coluna(df, coluna, n):
|
| 1487 |
+
"""Testa critérios de frequência mínima por categoria para uma coluna.
|
| 1488 |
+
|
| 1489 |
+
Retorna dict com 'valido' (bool) e 'mensagens' (list).
|
| 1490 |
+
"""
|
| 1491 |
+
if coluna not in df.columns:
|
| 1492 |
+
return {"valido": True, "mensagens": []}
|
| 1493 |
+
|
| 1494 |
+
frequencia = df[coluna].value_counts()
|
| 1495 |
+
ni_valido = True
|
| 1496 |
+
mensagens = []
|
| 1497 |
+
|
| 1498 |
+
for categoria, ni in frequencia.items():
|
| 1499 |
+
if n <= 30:
|
| 1500 |
+
if ni < 3:
|
| 1501 |
+
ni_valido = False
|
| 1502 |
+
mensagens.append(f"ni={ni} < 3 na categoria {categoria}")
|
| 1503 |
+
else:
|
| 1504 |
+
mensagens.append(f"ni={ni} ≥ 3 na categoria {categoria}")
|
| 1505 |
+
elif 30 < n <= 100:
|
| 1506 |
+
if ni < 0.1 * n:
|
| 1507 |
+
ni_valido = False
|
| 1508 |
+
mensagens.append(f"ni={ni} < {int(0.1 * n)} (10% de n) na categoria {categoria}")
|
| 1509 |
+
else:
|
| 1510 |
+
mensagens.append(f"ni={ni} ≥ {int(0.1 * n)} (10% de n) na categoria {categoria}")
|
| 1511 |
+
elif n > 100:
|
| 1512 |
+
if ni <= 10:
|
| 1513 |
+
ni_valido = False
|
| 1514 |
+
mensagens.append(f"ni={ni} ≤ 10 na categoria {categoria}")
|
| 1515 |
+
else:
|
| 1516 |
+
mensagens.append(f"ni={ni} > 10 na categoria {categoria}")
|
| 1517 |
+
|
| 1518 |
+
return {"valido": ni_valido, "mensagens": mensagens}
|
| 1519 |
+
|
| 1520 |
+
|
| 1521 |
+
def testar_micronumerosidade(df, colunas_x, dicotomicas=None, codigo_alocado=None):
|
| 1522 |
+
"""
|
| 1523 |
+
Testa critérios de micronumerosidade da NBR 14.653-2.
|
| 1524 |
+
|
| 1525 |
+
Usa as listas de dicotômicas e códigos alocados dos checkboxes (não auto-detecta por dtype).
|
| 1526 |
+
"""
|
| 1527 |
+
dicotomicas = dicotomicas or []
|
| 1528 |
+
codigo_alocado = codigo_alocado or []
|
| 1529 |
+
|
| 1530 |
+
n = len(df)
|
| 1531 |
+
k = len(colunas_x)
|
| 1532 |
+
|
| 1533 |
+
# Condição geral: n >= 3*(k+1)
|
| 1534 |
+
condicao_geral = n >= 3 * (k + 1)
|
| 1535 |
+
|
| 1536 |
+
resultados_dicotomicas = {}
|
| 1537 |
+
for coluna in dicotomicas:
|
| 1538 |
+
resultados_dicotomicas[coluna] = _testar_frequencia_coluna(df, coluna, n)
|
| 1539 |
+
|
| 1540 |
+
resultados_codigo_alocado = {}
|
| 1541 |
+
for coluna in codigo_alocado:
|
| 1542 |
+
resultados_codigo_alocado[coluna] = _testar_frequencia_coluna(df, coluna, n)
|
| 1543 |
+
|
| 1544 |
+
return {
|
| 1545 |
+
"n": n,
|
| 1546 |
+
"k": k,
|
| 1547 |
+
"condicao_geral_ok": condicao_geral,
|
| 1548 |
+
"dicotomicas": resultados_dicotomicas,
|
| 1549 |
+
"codigo_alocado": resultados_codigo_alocado,
|
| 1550 |
+
}
|
| 1551 |
+
|
| 1552 |
+
|
| 1553 |
+
# ============================================================
|
| 1554 |
+
# EXPORTAR BASE TRATADA (CSV)
|
| 1555 |
+
# ============================================================
|
| 1556 |
+
|
| 1557 |
+
def exportar_base_csv(df):
|
| 1558 |
+
"""
|
| 1559 |
+
Exporta DataFrame para arquivo CSV temporário.
|
| 1560 |
+
|
| 1561 |
+
Parâmetros:
|
| 1562 |
+
df: DataFrame a exportar
|
| 1563 |
+
|
| 1564 |
+
Retorna:
|
| 1565 |
+
str: Caminho do arquivo CSV gerado
|
| 1566 |
+
"""
|
| 1567 |
+
import tempfile
|
| 1568 |
+
import os
|
| 1569 |
+
|
| 1570 |
+
if df is None or df.empty:
|
| 1571 |
+
return None
|
| 1572 |
+
|
| 1573 |
+
# Cria arquivo temporário
|
| 1574 |
+
fd, caminho = tempfile.mkstemp(suffix=".csv", prefix="base_tratada_")
|
| 1575 |
+
os.close(fd)
|
| 1576 |
+
|
| 1577 |
+
# Salva CSV
|
| 1578 |
+
df.to_csv(caminho, index=True, encoding="utf-8-sig", sep=";", decimal=",")
|
| 1579 |
+
|
| 1580 |
+
return caminho
|
| 1581 |
+
|
| 1582 |
+
|
| 1583 |
+
# ============================================================
|
| 1584 |
+
# EXPORTAR MODELO (.dai)
|
| 1585 |
+
# ============================================================
|
| 1586 |
+
|
| 1587 |
+
def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatisticas=None,
|
| 1588 |
+
nome_arquivo="", elaborador=None, outliers_excluidos=None):
|
| 1589 |
+
"""
|
| 1590 |
+
Exporta o modelo em formato .dai v2 (estrutura nested).
|
| 1591 |
+
|
| 1592 |
+
Parâmetros:
|
| 1593 |
+
resultado_modelo: dict com resultado do ajuste
|
| 1594 |
+
df_original: DataFrame filtrado (sem outliers) usado no ajuste
|
| 1595 |
+
df_completo: DataFrame original completo (com outliers) — para restauração futura
|
| 1596 |
+
estatisticas: DataFrame com estatísticas (mantido como DataFrame)
|
| 1597 |
+
nome_arquivo: nome do arquivo de saída
|
| 1598 |
+
elaborador: dict com dados do avaliador (ou None)
|
| 1599 |
+
outliers_excluidos: lista de índices excluídos como outliers (ou None)
|
| 1600 |
+
|
| 1601 |
+
Retorna:
|
| 1602 |
+
tuple: (caminho_arquivo, mensagem)
|
| 1603 |
+
"""
|
| 1604 |
+
import tempfile
|
| 1605 |
+
|
| 1606 |
+
if resultado_modelo is None:
|
| 1607 |
+
return None, "Nenhum modelo para exportar"
|
| 1608 |
+
|
| 1609 |
+
try:
|
| 1610 |
+
# Função auxiliar para converter StringDtype para object e limpar DataFrame
|
| 1611 |
+
def limpar_df_para_export(df):
|
| 1612 |
+
"""Converte StringDtype para object e remove colunas Unnamed."""
|
| 1613 |
+
df_clean = df.copy()
|
| 1614 |
+
# Remove colunas 'Unnamed: X'
|
| 1615 |
+
cols_unnamed = [c for c in df_clean.columns if str(c).startswith('Unnamed')]
|
| 1616 |
+
if cols_unnamed:
|
| 1617 |
+
df_clean = df_clean.drop(columns=cols_unnamed)
|
| 1618 |
+
# Converte StringDtype para object (compatibilidade entre versões do pandas)
|
| 1619 |
+
for col in df_clean.columns:
|
| 1620 |
+
if pd.api.types.is_string_dtype(df_clean[col]):
|
| 1621 |
+
df_clean[col] = df_clean[col].astype(object)
|
| 1622 |
+
# Converte index se for string dtype (compatibilidade com .dai)
|
| 1623 |
+
if pd.api.types.is_string_dtype(df_clean.index):
|
| 1624 |
+
df_clean.index = df_clean.index.astype(object)
|
| 1625 |
+
return df_clean
|
| 1626 |
+
|
| 1627 |
+
# Monta pacote
|
| 1628 |
+
lat_col, lon_col = identificar_colunas_coords(df_original)
|
| 1629 |
+
|
| 1630 |
+
# DataFrame com coords para o mapa
|
| 1631 |
+
df_coords = limpar_df_para_export(df_original)
|
| 1632 |
+
if lat_col:
|
| 1633 |
+
df_coords = df_coords.rename(columns={lat_col: "lat"})
|
| 1634 |
+
if lon_col:
|
| 1635 |
+
df_coords = df_coords.rename(columns={lon_col: "lon"})
|
| 1636 |
+
|
| 1637 |
+
# Filtra apenas índices usados no modelo
|
| 1638 |
+
indices_modelo = resultado_modelo["indices_usados"]
|
| 1639 |
+
df_modelo = df_coords.loc[indices_modelo] if indices_modelo else df_coords
|
| 1640 |
+
df_modelo.index.name = None # Remove nome do índice
|
| 1641 |
+
|
| 1642 |
+
# Prepara df_completo (DataFrame original com todos os dados, inclusive outliers)
|
| 1643 |
+
if df_completo is not None:
|
| 1644 |
+
df_completo_export = limpar_df_para_export(df_completo)
|
| 1645 |
+
if lat_col:
|
| 1646 |
+
df_completo_export = df_completo_export.rename(columns={lat_col: "lat"})
|
| 1647 |
+
if lon_col:
|
| 1648 |
+
df_completo_export = df_completo_export.rename(columns={lon_col: "lon"})
|
| 1649 |
+
df_completo_export.index.name = None
|
| 1650 |
+
else:
|
| 1651 |
+
df_completo_export = None
|
| 1652 |
+
|
| 1653 |
+
# Prepara tabela_coef no formato compatível (índice = Variável, sem coluna "Variável")
|
| 1654 |
+
tabela_coef_export = limpar_df_para_export(resultado_modelo["tabela_coef"])
|
| 1655 |
+
if "Variável" in tabela_coef_export.columns:
|
| 1656 |
+
tabela_coef_export = tabela_coef_export.set_index("Variável")
|
| 1657 |
+
tabela_coef_export.index.name = None # Remove nome do índice
|
| 1658 |
+
# Converte index para object após set_index (compatibilidade .dai)
|
| 1659 |
+
if pd.api.types.is_string_dtype(tabela_coef_export.index):
|
| 1660 |
+
tabela_coef_export.index = tabela_coef_export.index.astype(object)
|
| 1661 |
+
# Renomeia p-valor para p-value
|
| 1662 |
+
tabela_coef_export = tabela_coef_export.rename(columns={"p-valor": "p-value"})
|
| 1663 |
+
|
| 1664 |
+
# Prepara tabela_obs_calc no formato compatível
|
| 1665 |
+
tabela_obs_calc_export = limpar_df_para_export(resultado_modelo["tabela_obs_calc"])
|
| 1666 |
+
# Move Índice para o index do DataFrame
|
| 1667 |
+
if "Índice" in tabela_obs_calc_export.columns:
|
| 1668 |
+
tabela_obs_calc_export = tabela_obs_calc_export.set_index("Índice")
|
| 1669 |
+
tabela_obs_calc_export.index.name = None # Remove nome do índice
|
| 1670 |
+
# Renomeia colunas para formato compatível
|
| 1671 |
+
tabela_obs_calc_export = tabela_obs_calc_export.rename(columns={
|
| 1672 |
+
"Resíduo Pad.": "Resíduo Padronizado",
|
| 1673 |
+
"Resíduo Stud.": "Resíduo Studentizado",
|
| 1674 |
+
"Cook": "Distância de Cook"
|
| 1675 |
+
})
|
| 1676 |
+
|
| 1677 |
+
# Extrai diagnosticos do resultado do modelo
|
| 1678 |
+
diag = resultado_modelo["diagnosticos"].copy()
|
| 1679 |
+
|
| 1680 |
+
# Prepara estatisticas no formato compatível (índice = Variável)
|
| 1681 |
+
estatisticas_df = estatisticas if isinstance(estatisticas, pd.DataFrame) else pd.DataFrame(estatisticas)
|
| 1682 |
+
estatisticas_export = limpar_df_para_export(estatisticas_df)
|
| 1683 |
+
if "Variável" in estatisticas_export.columns:
|
| 1684 |
+
estatisticas_export = estatisticas_export.set_index("Variável")
|
| 1685 |
+
estatisticas_export.index.name = None # Remove nome do índice
|
| 1686 |
+
# Converte index para object após set_index (compatibilidade .dai)
|
| 1687 |
+
if pd.api.types.is_string_dtype(estatisticas_export.index):
|
| 1688 |
+
estatisticas_export.index = estatisticas_export.index.astype(object)
|
| 1689 |
+
# Adiciona coluna Tipo (dtype) se não existir
|
| 1690 |
+
if "Tipo" not in estatisticas_export.columns:
|
| 1691 |
+
# Obtém tipos das colunas do DataFrame original
|
| 1692 |
+
tipos = {}
|
| 1693 |
+
for var in estatisticas_export.index:
|
| 1694 |
+
if var in df_original.columns:
|
| 1695 |
+
tipos[var] = str(df_original[var].dtype)
|
| 1696 |
+
else:
|
| 1697 |
+
tipos[var] = "unknown"
|
| 1698 |
+
estatisticas_export.insert(0, "Tipo", pd.Series(tipos))
|
| 1699 |
+
# Mantém apenas colunas compatíveis (na ordem correta)
|
| 1700 |
+
colunas_manter = ["Tipo", "Contagem", "Média", "Mediana", "Mínimo", "Máximo", "Desvio Padrão"]
|
| 1701 |
+
colunas_existentes = [c for c in colunas_manter if c in estatisticas_export.columns]
|
| 1702 |
+
estatisticas_export = estatisticas_export[colunas_existentes]
|
| 1703 |
+
|
| 1704 |
+
# Converte dtypes para compatibilidade com formato .dai
|
| 1705 |
+
if "Contagem" in estatisticas_export.columns:
|
| 1706 |
+
estatisticas_export["Contagem"] = estatisticas_export["Contagem"].astype(str).astype(object)
|
| 1707 |
+
if "Tipo" in estatisticas_export.columns:
|
| 1708 |
+
estatisticas_export["Tipo"] = estatisticas_export["Tipo"].astype(object)
|
| 1709 |
+
|
| 1710 |
+
# Limpa top_X_esc e top_y_esc
|
| 1711 |
+
top_X_esc_export = limpar_df_para_export(resultado_modelo["X_transformado"])
|
| 1712 |
+
top_X_esc_export.index.name = None # Remove nome do índice
|
| 1713 |
+
top_y_esc_export = resultado_modelo["y_transformado"].copy()
|
| 1714 |
+
top_y_esc_export.index.name = None # Remove nome do índice
|
| 1715 |
+
|
| 1716 |
+
pacote = {
|
| 1717 |
+
"versao": 2,
|
| 1718 |
+
"elaborador": elaborador,
|
| 1719 |
+
"dados": {
|
| 1720 |
+
"df": df_modelo,
|
| 1721 |
+
"df_completo": df_completo_export,
|
| 1722 |
+
"outliers_excluidos": outliers_excluidos or [],
|
| 1723 |
+
"estatisticas": estatisticas_export,
|
| 1724 |
+
},
|
| 1725 |
+
"transformacoes": {
|
| 1726 |
+
"info": [resultado_modelo["y_esc_info"]] + resultado_modelo["X_esc_info"],
|
| 1727 |
+
"X": top_X_esc_export,
|
| 1728 |
+
"y": top_y_esc_export,
|
| 1729 |
+
"dicotomicas": resultado_modelo.get("dicotomicas", []),
|
| 1730 |
+
"codigo_alocado": resultado_modelo.get("codigo_alocado", []),
|
| 1731 |
+
"percentuais": resultado_modelo.get("percentuais", []),
|
| 1732 |
+
},
|
| 1733 |
+
"modelo": {
|
| 1734 |
+
"sm": resultado_modelo["modelo_sm"],
|
| 1735 |
+
"coeficientes": tabela_coef_export,
|
| 1736 |
+
"obs_calc": tabela_obs_calc_export,
|
| 1737 |
+
"diagnosticos": {
|
| 1738 |
+
"gerais": {
|
| 1739 |
+
"n": diag.get("n"),
|
| 1740 |
+
"k": diag.get("k"),
|
| 1741 |
+
"desvio_padrao_residuos": diag.get("desvio_padrao_residuos"),
|
| 1742 |
+
"mse": diag.get("mse"),
|
| 1743 |
+
"r2": diag.get("r2"),
|
| 1744 |
+
"r2_ajustado": diag.get("r2_ajustado"),
|
| 1745 |
+
"r_pearson": diag.get("r_pearson"),
|
| 1746 |
+
},
|
| 1747 |
+
"teste_f": {
|
| 1748 |
+
"estatistica": diag.get("Fc"),
|
| 1749 |
+
"p_valor": diag.get("p_valor_F"),
|
| 1750 |
+
"interpretacao": diag.get("interp_F"),
|
| 1751 |
+
},
|
| 1752 |
+
"teste_ks": {
|
| 1753 |
+
"estatistica": diag.get("ks_stat"),
|
| 1754 |
+
"p_valor": diag.get("ks_p"),
|
| 1755 |
+
"interpretacao": diag.get("interp_KS"),
|
| 1756 |
+
},
|
| 1757 |
+
"teste_normalidade": {
|
| 1758 |
+
"percentuais": diag.get("perc_resid"),
|
| 1759 |
+
},
|
| 1760 |
+
"teste_dw": {
|
| 1761 |
+
"estatistica": diag.get("dw"),
|
| 1762 |
+
"interpretacao": diag.get("interp_DW"),
|
| 1763 |
+
},
|
| 1764 |
+
"teste_bp": {
|
| 1765 |
+
"estatistica": diag.get("bp_lm"),
|
| 1766 |
+
"p_valor": diag.get("bp_p"),
|
| 1767 |
+
"interpretacao": diag.get("interp_BP"),
|
| 1768 |
+
},
|
| 1769 |
+
"equacao": diag.get("equacao"),
|
| 1770 |
+
},
|
| 1771 |
+
},
|
| 1772 |
+
}
|
| 1773 |
+
|
| 1774 |
+
# Função para converter recursivamente StringDtype para object (compatibilidade HF)
|
| 1775 |
+
def converter_para_serializacao(obj):
|
| 1776 |
+
"""Converte recursivamente todos os StringDtype para object dtype."""
|
| 1777 |
+
if isinstance(obj, pd.DataFrame):
|
| 1778 |
+
df = obj.copy()
|
| 1779 |
+
if pd.api.types.is_string_dtype(df.index):
|
| 1780 |
+
df.index = df.index.astype(object)
|
| 1781 |
+
for col in df.columns:
|
| 1782 |
+
if pd.api.types.is_string_dtype(df[col]):
|
| 1783 |
+
df[col] = df[col].astype(object)
|
| 1784 |
+
return df
|
| 1785 |
+
elif isinstance(obj, pd.Series):
|
| 1786 |
+
s = obj.copy()
|
| 1787 |
+
if pd.api.types.is_string_dtype(s):
|
| 1788 |
+
s = s.astype(object)
|
| 1789 |
+
if pd.api.types.is_string_dtype(s.index):
|
| 1790 |
+
s.index = s.index.astype(object)
|
| 1791 |
+
return s
|
| 1792 |
+
elif isinstance(obj, dict):
|
| 1793 |
+
return {k: converter_para_serializacao(v) for k, v in obj.items()}
|
| 1794 |
+
elif isinstance(obj, list):
|
| 1795 |
+
return [converter_para_serializacao(v) for v in obj]
|
| 1796 |
+
else:
|
| 1797 |
+
return obj
|
| 1798 |
+
|
| 1799 |
+
# Converte todo o pacote para garantir compatibilidade entre versões do pandas
|
| 1800 |
+
pacote = converter_para_serializacao(pacote)
|
| 1801 |
+
|
| 1802 |
+
# Cria arquivo no diretório temporário com o nome exato dado pelo usuário
|
| 1803 |
+
nome_base = nome_arquivo.replace(".dai", "") if nome_arquivo.endswith(".dai") else nome_arquivo
|
| 1804 |
+
caminho = os.path.join(tempfile.gettempdir(), f"{nome_base}.dai")
|
| 1805 |
+
|
| 1806 |
+
dump(pacote, caminho)
|
| 1807 |
+
|
| 1808 |
+
return caminho, f"Modelo pronto para download"
|
| 1809 |
+
|
| 1810 |
+
except Exception as e:
|
| 1811 |
+
return None, f"Erro ao exportar: {str(e)}"
|
| 1812 |
+
|
| 1813 |
+
|
| 1814 |
+
# ============================================================
|
| 1815 |
+
# EXPORTAÇÃO DE AVALIAÇÕES EM EXCEL
|
| 1816 |
+
# ============================================================
|
| 1817 |
+
|
| 1818 |
+
def exportar_avaliacoes_excel(avaliacoes_lista):
|
| 1819 |
+
"""Exporta lista de avaliações como arquivo Excel.
|
| 1820 |
+
|
| 1821 |
+
Args:
|
| 1822 |
+
avaliacoes_lista: lista de dicts retornados por avaliar_imovel().
|
| 1823 |
+
|
| 1824 |
+
Returns:
|
| 1825 |
+
str caminho do arquivo temporário, ou None se lista vazia.
|
| 1826 |
+
"""
|
| 1827 |
+
import tempfile
|
| 1828 |
+
import pandas as pd
|
| 1829 |
+
|
| 1830 |
+
if not avaliacoes_lista:
|
| 1831 |
+
return None
|
| 1832 |
+
|
| 1833 |
+
n = len(avaliacoes_lista)
|
| 1834 |
+
colunas_x = list(avaliacoes_lista[0]["valores_x"].keys())
|
| 1835 |
+
|
| 1836 |
+
# Montar dados: linhas = variáveis + métricas, colunas = avaliações
|
| 1837 |
+
dados = {}
|
| 1838 |
+
for i, aval in enumerate(avaliacoes_lista):
|
| 1839 |
+
col_name = f"Aval. {i + 1}"
|
| 1840 |
+
valores = []
|
| 1841 |
+
# Variáveis X
|
| 1842 |
+
for var in colunas_x:
|
| 1843 |
+
valores.append(aval["valores_x"].get(var, ""))
|
| 1844 |
+
# Métricas
|
| 1845 |
+
valores.append(aval["estimado"])
|
| 1846 |
+
valores.append(aval["ca_inf"])
|
| 1847 |
+
valores.append(aval["ca_sup"])
|
| 1848 |
+
valores.append(aval["ic_inf"])
|
| 1849 |
+
valores.append(aval["ic_sup"])
|
| 1850 |
+
valores.append(f'{aval["perc_inf"]:.1f}%')
|
| 1851 |
+
valores.append(f'{aval["perc_sup"]:.1f}%')
|
| 1852 |
+
valores.append(f'{aval["amplitude"]:.1f}%')
|
| 1853 |
+
valores.append(aval["precisao"])
|
| 1854 |
+
valores.append(aval["fundamentacao"])
|
| 1855 |
+
dados[col_name] = valores
|
| 1856 |
+
|
| 1857 |
+
indice = colunas_x + [
|
| 1858 |
+
"Estimado", "CA −15%", "CA +15%",
|
| 1859 |
+
"IC 80% Inf.", "IC 80% Sup.",
|
| 1860 |
+
"% Inf.", "% Sup.", "Amplitude",
|
| 1861 |
+
"Precisão", "Fundamentação",
|
| 1862 |
+
]
|
| 1863 |
+
|
| 1864 |
+
df = pd.DataFrame(dados, index=indice)
|
| 1865 |
+
caminho = os.path.join(tempfile.gettempdir(), "avaliacoes_mesa.xlsx")
|
| 1866 |
+
df.to_excel(caminho, index=True)
|
| 1867 |
+
return caminho
|
| 1868 |
+
|
| 1869 |
+
|
| 1870 |
+
# ============================================================
|
| 1871 |
+
# AVALIAÇÃO DE IMÓVEL
|
| 1872 |
+
# ============================================================
|
| 1873 |
+
|
| 1874 |
+
def avaliar_imovel(modelo_sm, valores_x, colunas_x, transformacoes_x, transformacao_y, estatisticas_df, dicotomicas=None, codigo_alocado=None, percentuais=None):
|
| 1875 |
+
"""
|
| 1876 |
+
Avalia um imóvel com base no modelo ajustado, incluindo verificação de
|
| 1877 |
+
extrapolação e cálculo da fronteira amostral conforme NBR 14.653-2.
|
| 1878 |
+
|
| 1879 |
+
Args:
|
| 1880 |
+
modelo_sm: Objeto OLSResults do statsmodels.
|
| 1881 |
+
valores_x: dict {coluna: valor} com os valores de entrada.
|
| 1882 |
+
colunas_x: list de nomes das variáveis X (na ordem do modelo).
|
| 1883 |
+
transformacoes_x: dict {coluna: transformacao} para cada X.
|
| 1884 |
+
transformacao_y: str com a transformação aplicada em Y.
|
| 1885 |
+
estatisticas_df: DataFrame com colunas Variável, Mínimo, Máximo (entre outras).
|
| 1886 |
+
dicotomicas: list de nomes de colunas dicotômicas (0/1).
|
| 1887 |
+
codigo_alocado: list de nomes de colunas de código alocado/ajustado.
|
| 1888 |
+
percentuais: list de nomes de colunas percentuais (0 a 1).
|
| 1889 |
+
|
| 1890 |
+
Returns:
|
| 1891 |
+
dict com resultado da avaliação ou None em caso de erro.
|
| 1892 |
+
"""
|
| 1893 |
+
try:
|
| 1894 |
+
# --------------------------------------------------------
|
| 1895 |
+
# 1. MONTAR X_aval
|
| 1896 |
+
# --------------------------------------------------------
|
| 1897 |
+
X_aval = pd.DataFrame([[valores_x[col] for col in colunas_x]], columns=colunas_x)
|
| 1898 |
+
X_fronteira = X_aval.copy()
|
| 1899 |
+
|
| 1900 |
+
# --------------------------------------------------------
|
| 1901 |
+
# 2. VERIFICAR EXTRAPOLAÇÃO
|
| 1902 |
+
# --------------------------------------------------------
|
| 1903 |
+
# Obter limites das estatísticas
|
| 1904 |
+
if "Variável" in estatisticas_df.columns:
|
| 1905 |
+
limites = estatisticas_df.set_index("Variável")[["Mínimo", "Máximo"]].copy()
|
| 1906 |
+
else:
|
| 1907 |
+
limites = estatisticas_df[["Mínimo", "Máximo"]].copy()
|
| 1908 |
+
|
| 1909 |
+
for col in limites.columns:
|
| 1910 |
+
limites[col] = pd.to_numeric(limites[col], errors="coerce")
|
| 1911 |
+
|
| 1912 |
+
extrapolacoes = {}
|
| 1913 |
+
houve_extrapolacao = False
|
| 1914 |
+
qtd_extrapolacoes = 0
|
| 1915 |
+
qtd_extrapolacoes_acima_limites = 0
|
| 1916 |
+
qtd_extrapolacoes_dentro_limites = 0
|
| 1917 |
+
|
| 1918 |
+
for col in colunas_x:
|
| 1919 |
+
val = float(X_aval[col].values[0])
|
| 1920 |
+
X_fronteira[col] = val
|
| 1921 |
+
extrapolacoes[col] = {"status": "ok", "percentual": 0.0, "direcao": "ok"}
|
| 1922 |
+
|
| 1923 |
+
if col not in limites.index:
|
| 1924 |
+
continue
|
| 1925 |
+
|
| 1926 |
+
min_val = float(limites.loc[col, "Mínimo"])
|
| 1927 |
+
max_val = float(limites.loc[col, "Máximo"])
|
| 1928 |
+
|
| 1929 |
+
# Dicotômica, código alocado ou percentual — validação já feita no callback
|
| 1930 |
+
if col in (dicotomicas or []):
|
| 1931 |
+
extrapolacoes[col] = {"status": "dicotomica", "percentual": 0.0, "direcao": "ok"}
|
| 1932 |
+
continue
|
| 1933 |
+
if col in (codigo_alocado or []):
|
| 1934 |
+
extrapolacoes[col] = {"status": "codigo_alocado", "percentual": 0.0, "direcao": "ok"}
|
| 1935 |
+
continue
|
| 1936 |
+
if col in (percentuais or []):
|
| 1937 |
+
extrapolacoes[col] = {"status": "percentual", "percentual": 0.0, "direcao": "ok"}
|
| 1938 |
+
continue
|
| 1939 |
+
|
| 1940 |
+
# Acima do máximo
|
| 1941 |
+
if val > max_val:
|
| 1942 |
+
houve_extrapolacao = True
|
| 1943 |
+
qtd_extrapolacoes += 1
|
| 1944 |
+
X_fronteira[col] = max_val
|
| 1945 |
+
percentual = round(((val / max_val) - 1) * 100, 1) if max_val != 0 else 0.0
|
| 1946 |
+
|
| 1947 |
+
if percentual > 100:
|
| 1948 |
+
qtd_extrapolacoes_acima_limites += 1
|
| 1949 |
+
extrapolacoes[col] = {"status": "grave", "percentual": percentual, "direcao": "acima"}
|
| 1950 |
+
else:
|
| 1951 |
+
qtd_extrapolacoes_dentro_limites += 1
|
| 1952 |
+
extrapolacoes[col] = {"status": "warning", "percentual": percentual, "direcao": "acima"}
|
| 1953 |
+
|
| 1954 |
+
# Abaixo do mínimo
|
| 1955 |
+
elif val < min_val:
|
| 1956 |
+
houve_extrapolacao = True
|
| 1957 |
+
qtd_extrapolacoes += 1
|
| 1958 |
+
X_fronteira[col] = min_val
|
| 1959 |
+
percentual = round((1 - (val / min_val)) * 100, 1) if min_val != 0 else 0.0
|
| 1960 |
+
|
| 1961 |
+
if percentual > 50:
|
| 1962 |
+
qtd_extrapolacoes_acima_limites += 1
|
| 1963 |
+
extrapolacoes[col] = {"status": "grave", "percentual": percentual, "direcao": "abaixo"}
|
| 1964 |
+
else:
|
| 1965 |
+
qtd_extrapolacoes_dentro_limites += 1
|
| 1966 |
+
extrapolacoes[col] = {"status": "warning", "percentual": percentual, "direcao": "abaixo"}
|
| 1967 |
+
|
| 1968 |
+
# --------------------------------------------------------
|
| 1969 |
+
# 3. APLICAR TRANSFORMAÇÕES
|
| 1970 |
+
# --------------------------------------------------------
|
| 1971 |
+
X_aval_transf = X_aval.copy()
|
| 1972 |
+
for col in colunas_x:
|
| 1973 |
+
transf = transformacoes_x.get(col, "(x)")
|
| 1974 |
+
X_aval_transf[col] = aplicar_transformacao(X_aval_transf[col], transf)
|
| 1975 |
+
|
| 1976 |
+
# --------------------------------------------------------
|
| 1977 |
+
# 4. ADICIONAR CONSTANTE E PREVER
|
| 1978 |
+
# --------------------------------------------------------
|
| 1979 |
+
X_aval_const = sm.add_constant(X_aval_transf, has_constant="add")
|
| 1980 |
+
X_aval_const = X_aval_const[modelo_sm.model.exog_names]
|
| 1981 |
+
|
| 1982 |
+
pred = modelo_sm.get_prediction(X_aval_const)
|
| 1983 |
+
y_mean = pred.predicted_mean
|
| 1984 |
+
alpha = 0.20 # IC 80% (NBR 14.653-2)
|
| 1985 |
+
ic = pred.conf_int(alpha=alpha)
|
| 1986 |
+
|
| 1987 |
+
# --------------------------------------------------------
|
| 1988 |
+
# 5. INVERTER TRANSFORMAÇÃO Y
|
| 1989 |
+
# --------------------------------------------------------
|
| 1990 |
+
y_pred = inverter_transformacao_y(y_mean, transformacao_y)
|
| 1991 |
+
y_ic_inf = inverter_transformacao_y(ic[:, 0], transformacao_y)
|
| 1992 |
+
y_ic_sup = inverter_transformacao_y(ic[:, 1], transformacao_y)
|
| 1993 |
+
|
| 1994 |
+
estimado = float(y_pred[0])
|
| 1995 |
+
ic_inf = float(y_ic_inf[0])
|
| 1996 |
+
ic_sup = float(y_ic_sup[0])
|
| 1997 |
+
|
| 1998 |
+
# --------------------------------------------------------
|
| 1999 |
+
# 6. CAMPO DE ARBÍTRIO (±15%, NBR 14.653-2)
|
| 2000 |
+
# --------------------------------------------------------
|
| 2001 |
+
ca_inf = estimado * 0.85
|
| 2002 |
+
ca_sup = estimado * 1.15
|
| 2003 |
+
|
| 2004 |
+
# --------------------------------------------------------
|
| 2005 |
+
# 7. AMPLITUDE E PRECISÃO (NBR 14.653-2 tabela 2)
|
| 2006 |
+
# --------------------------------------------------------
|
| 2007 |
+
perc_inf = ((estimado - ic_inf) / estimado) * 100 if estimado != 0 else 0.0
|
| 2008 |
+
perc_sup = ((ic_sup - estimado) / estimado) * 100 if estimado != 0 else 0.0
|
| 2009 |
+
amplitude = perc_inf + perc_sup
|
| 2010 |
+
|
| 2011 |
+
if amplitude > 50:
|
| 2012 |
+
precisao = "Sem enquadramento"
|
| 2013 |
+
elif amplitude > 40:
|
| 2014 |
+
precisao = "Grau I"
|
| 2015 |
+
elif amplitude > 30:
|
| 2016 |
+
precisao = "Grau II"
|
| 2017 |
+
else:
|
| 2018 |
+
precisao = "Grau III"
|
| 2019 |
+
|
| 2020 |
+
# --------------------------------------------------------
|
| 2021 |
+
# 8. FRONTEIRA AMOSTRAL (se houve extrapolação)
|
| 2022 |
+
# --------------------------------------------------------
|
| 2023 |
+
fronteira = None
|
| 2024 |
+
perc_ext = None
|
| 2025 |
+
fundamentacao = "Grau III"
|
| 2026 |
+
|
| 2027 |
+
if houve_extrapolacao:
|
| 2028 |
+
X_front_transf = X_fronteira.copy()
|
| 2029 |
+
for col in colunas_x:
|
| 2030 |
+
transf = transformacoes_x.get(col, "(x)")
|
| 2031 |
+
X_front_transf[col] = aplicar_transformacao(X_front_transf[col], transf)
|
| 2032 |
+
|
| 2033 |
+
X_front_const = sm.add_constant(X_front_transf, has_constant="add")
|
| 2034 |
+
X_front_const = X_front_const[modelo_sm.model.exog_names]
|
| 2035 |
+
|
| 2036 |
+
pred_front = modelo_sm.get_prediction(X_front_const)
|
| 2037 |
+
y_fronteira = inverter_transformacao_y(pred_front.predicted_mean, transformacao_y)
|
| 2038 |
+
fronteira = float(y_fronteira[0])
|
| 2039 |
+
|
| 2040 |
+
perc_ext = float(abs(((fronteira - estimado) / fronteira) * 100)) if fronteira != 0 else 0.0
|
| 2041 |
+
|
| 2042 |
+
# --------------------------------------------------------
|
| 2043 |
+
# 9. FUNDAMENTAÇÃO (NBR 14.653-2 tabela 1)
|
| 2044 |
+
# --------------------------------------------------------
|
| 2045 |
+
if qtd_extrapolacoes == 0:
|
| 2046 |
+
fundamentacao = "Grau III"
|
| 2047 |
+
elif qtd_extrapolacoes_acima_limites > 0:
|
| 2048 |
+
fundamentacao = "Sem enquadramento"
|
| 2049 |
+
elif perc_ext > 20:
|
| 2050 |
+
fundamentacao = "Sem enquadramento"
|
| 2051 |
+
elif qtd_extrapolacoes == 1:
|
| 2052 |
+
fundamentacao = "Grau I" if perc_ext > 15 else "Grau II"
|
| 2053 |
+
else: # >1 extrapolação, sem grave, impacto ≤20%
|
| 2054 |
+
fundamentacao = "Grau I"
|
| 2055 |
+
|
| 2056 |
+
return {
|
| 2057 |
+
"valores_x": {col: float(valores_x[col]) for col in colunas_x},
|
| 2058 |
+
"extrapolacoes": extrapolacoes,
|
| 2059 |
+
"estimado": estimado,
|
| 2060 |
+
"ca_inf": ca_inf,
|
| 2061 |
+
"ca_sup": ca_sup,
|
| 2062 |
+
"ic_inf": ic_inf,
|
| 2063 |
+
"ic_sup": ic_sup,
|
| 2064 |
+
"perc_inf": round(perc_inf, 2),
|
| 2065 |
+
"perc_sup": round(perc_sup, 2),
|
| 2066 |
+
"amplitude": round(amplitude, 2),
|
| 2067 |
+
"precisao": precisao,
|
| 2068 |
+
"houve_extrapolacao": houve_extrapolacao,
|
| 2069 |
+
"fronteira": fronteira,
|
| 2070 |
+
"perc_ext": round(perc_ext, 2) if perc_ext is not None else None,
|
| 2071 |
+
"qtd_extrapolacoes": qtd_extrapolacoes,
|
| 2072 |
+
"fundamentacao": fundamentacao,
|
| 2073 |
+
}
|
| 2074 |
+
|
| 2075 |
+
except Exception as e:
|
| 2076 |
+
print(f"Erro ao avaliar imóvel: {e}")
|
| 2077 |
+
return None
|
backend/app/core/elaboracao/formatadores.py
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
formatadores.py - Formatação HTML e helpers de exibição para a aba Elaboração.
|
| 4 |
+
|
| 5 |
+
Funções puras: recebem dados, retornam strings HTML/texto.
|
| 6 |
+
Sem dependência de Gradio.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ============================================================
|
| 15 |
+
# CONSTANTES
|
| 16 |
+
# ============================================================
|
| 17 |
+
|
| 18 |
+
TITULO = """
|
| 19 |
+
# ELABORAÇÃO DE MODELOS ESTATÍSTICOS
|
| 20 |
+
### Divisão de Avaliação de Imóveis
|
| 21 |
+
---
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ============================================================
|
| 26 |
+
# FUNÇÕES DE FORMATAÇÃO
|
| 27 |
+
# ============================================================
|
| 28 |
+
|
| 29 |
+
def arredondar_df(df, decimais=4):
|
| 30 |
+
"""Arredonda apenas colunas numéricas de um DataFrame e adiciona coluna Índice."""
|
| 31 |
+
if df is None:
|
| 32 |
+
return None
|
| 33 |
+
df_copy = df.copy()
|
| 34 |
+
# Adiciona índice como primeira coluna (se não existir)
|
| 35 |
+
if "Índice" not in df_copy.columns:
|
| 36 |
+
df_copy.insert(0, "Índice", df_copy.index)
|
| 37 |
+
colunas_numericas = df_copy.select_dtypes(include=[np.number]).columns
|
| 38 |
+
# Não arredonda a coluna Índice
|
| 39 |
+
colunas_numericas = [c for c in colunas_numericas if c != "Índice"]
|
| 40 |
+
df_copy[colunas_numericas] = df_copy[colunas_numericas].round(decimais)
|
| 41 |
+
return df_copy
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def carregar_css():
|
| 45 |
+
"""Carrega CSS externo."""
|
| 46 |
+
css_path = os.path.join(os.path.dirname(__file__), "styles.css")
|
| 47 |
+
try:
|
| 48 |
+
with open(css_path, "r", encoding="utf-8") as f:
|
| 49 |
+
return f.read()
|
| 50 |
+
except FileNotFoundError:
|
| 51 |
+
return ""
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def criar_header_secao(numero: int, titulo: str, timestamp: str = "") -> str:
|
| 55 |
+
"""Cria um header HTML estilizado para seções, opcionalmente com timestamp."""
|
| 56 |
+
timestamp_html = f' <span class="section-timestamp">(Atualizado às {timestamp})</span>' if timestamp else ""
|
| 57 |
+
return f'''
|
| 58 |
+
<div class="section-header">
|
| 59 |
+
<span class="section-number">{numero}</span>
|
| 60 |
+
<h2 class="section-title">{titulo}{timestamp_html}</h2>
|
| 61 |
+
</div>
|
| 62 |
+
'''
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def formatar_lista_variaveis_html(info_variaveis):
|
| 66 |
+
"""Gera o bloco HTML da lista de variáveis (dependente + independentes).
|
| 67 |
+
|
| 68 |
+
info_variaveis: lista de strings no formato ["Y_NOME: transf", "X1: transf", ...]
|
| 69 |
+
Retorna HTML pronto para ser inserido como lado direito do badge do elaborador.
|
| 70 |
+
"""
|
| 71 |
+
if not info_variaveis:
|
| 72 |
+
return ""
|
| 73 |
+
|
| 74 |
+
y_str = info_variaveis[0]
|
| 75 |
+
y_parts = y_str.split(": ", 1)
|
| 76 |
+
y_nome = y_parts[0].strip()
|
| 77 |
+
y_transf = y_parts[1].strip() if len(y_parts) > 1 else ""
|
| 78 |
+
y_transf_display = "" if y_transf in ("(y)", "(x)", "y", "x", "") else y_transf
|
| 79 |
+
y_badge = (
|
| 80 |
+
"<span style='background:#cce5ff;color:#004085;border-radius:4px;"
|
| 81 |
+
f"padding:3px 10px;font-size:1em;font-weight:600;'>{y_nome}"
|
| 82 |
+
+ (f" <span style='font-weight:400;font-size:1em;'>{y_transf_display}</span>"
|
| 83 |
+
if y_transf_display else "")
|
| 84 |
+
+ "</span>"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
x_badges = []
|
| 88 |
+
for item in info_variaveis[1:]:
|
| 89 |
+
parts = item.split(": ", 1)
|
| 90 |
+
nome = parts[0].strip()
|
| 91 |
+
transf = parts[1].strip() if len(parts) > 1 else ""
|
| 92 |
+
transf_display = "" if transf in ("(x)", "(y)", "x", "y", "") else transf
|
| 93 |
+
badge = (
|
| 94 |
+
"<span style='background:#ced4da;color:#343a40;border-radius:4px;"
|
| 95 |
+
f"padding:3px 8px;font-size:1em;display:inline-block;margin:2px 3px 2px 0;'>{nome}"
|
| 96 |
+
+ (f" <span style='color:#6c757d;font-size:1em;'>{transf_display}</span>"
|
| 97 |
+
if transf_display else "")
|
| 98 |
+
+ "</span>"
|
| 99 |
+
)
|
| 100 |
+
x_badges.append(badge)
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
"<div style='font-size:0.9em;line-height:2;'>"
|
| 104 |
+
"<div><span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;'>Variável Dependente:</span>"
|
| 105 |
+
+ y_badge + "</div>"
|
| 106 |
+
"<div style='margin-top:4px;'>"
|
| 107 |
+
"<span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;'>Variáveis Independentes:</span>"
|
| 108 |
+
+ "".join(x_badges) + "</div>"
|
| 109 |
+
"</div>"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def formatar_diagnosticos_html(diagnosticos):
|
| 114 |
+
"""Formata diagnósticos como HTML no estilo field-row do MODELOS_DAI."""
|
| 115 |
+
if not diagnosticos:
|
| 116 |
+
return "<p>Nenhum modelo ajustado.</p>"
|
| 117 |
+
|
| 118 |
+
html = '<div class="dai-card">'
|
| 119 |
+
|
| 120 |
+
# Estatísticas Gerais
|
| 121 |
+
html += '<div class="section-title-orange">Estatísticas Gerais</div>'
|
| 122 |
+
html += f'''<div class="field-row"><span class="field-row-label">Número de observações</span><span class="field-row-value">{diagnosticos["n"]}</span></div>'''
|
| 123 |
+
html += f'''<div class="field-row"><span class="field-row-label">Número de variáveis independentes</span><span class="field-row-value">{diagnosticos["k"]}</span></div>'''
|
| 124 |
+
html += f'''<div class="field-row"><span class="field-row-label">Desvio padrão dos resíduos</span><span class="field-row-value">{diagnosticos["desvio_padrao_residuos"]:.4f}</span></div>'''
|
| 125 |
+
html += f'''<div class="field-row"><span class="field-row-label">MSE</span><span class="field-row-value">{diagnosticos["mse"]:.4f}</span></div>'''
|
| 126 |
+
html += f'''<div class="field-row"><span class="field-row-label">R²</span><span class="field-row-value">{diagnosticos["r2"]:.4f}</span></div>'''
|
| 127 |
+
html += f'''<div class="field-row"><span class="field-row-label">R² ajustado</span><span class="field-row-value">{diagnosticos["r2_ajustado"]:.4f}</span></div>'''
|
| 128 |
+
html += f'''<div class="field-row"><span class="field-row-label">Correlação Pearson</span><span class="field-row-value">{diagnosticos["r_pearson"]:.4f}</span></div>'''
|
| 129 |
+
|
| 130 |
+
# Teste F
|
| 131 |
+
html += '<div class="section-title-orange">Teste F</div>'
|
| 132 |
+
html += f'''<div class="field-row"><span class="field-row-label">Estatística F</span><span class="field-row-value">{diagnosticos["Fc"]:.4f}</span></div>'''
|
| 133 |
+
html += f'''<div class="field-row"><span class="field-row-label">P-valor</span><span class="field-row-value">{diagnosticos["p_valor_F"]:.4f}</span></div>'''
|
| 134 |
+
html += f'''<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value">{diagnosticos["interp_F"]}</span></div>'''
|
| 135 |
+
|
| 136 |
+
# Teste de Normalidade (KS)
|
| 137 |
+
html += '<div class="section-title-orange">Teste de Normalidade (Kolmogorov-Smirnov)</div>'
|
| 138 |
+
html += f'''<div class="field-row"><span class="field-row-label">Estatística KS</span><span class="field-row-value">{diagnosticos["ks_stat"]:.4f}</span></div>'''
|
| 139 |
+
html += f'''<div class="field-row"><span class="field-row-label">P-valor</span><span class="field-row-value">{diagnosticos["ks_p"]:.4f}</span></div>'''
|
| 140 |
+
html += f'''<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value">{diagnosticos["interp_KS"]}</span></div>'''
|
| 141 |
+
|
| 142 |
+
# Teste de Normalidade (Curva Normal)
|
| 143 |
+
html += '<div class="section-title-orange">Teste de Normalidade (Comparação com a Curva Normal)</div>'
|
| 144 |
+
html += f'''<div class="field-row"><span class="field-row-label">Percentuais Atingidos</span><span class="field-row-value">{diagnosticos["perc_resid"]}</span></div>'''
|
| 145 |
+
html += '<div class="interpretation-label">Interpretação</div>'
|
| 146 |
+
html += '<div class="interpretation-item">• Ideal 68% → aceitável entre 64% e 75%</div>'
|
| 147 |
+
html += '<div class="interpretation-item">• Ideal 90% → aceitável entre 88% e 95%</div>'
|
| 148 |
+
html += '<div class="interpretation-item">• Ideal 95% → aceitável entre 95% e 100%</div>'
|
| 149 |
+
|
| 150 |
+
# Teste de Autocorrelação (Durbin-Watson)
|
| 151 |
+
html += '<div class="section-title-orange">Teste de Autocorrelação (Durbin-Watson)</div>'
|
| 152 |
+
html += f'''<div class="field-row"><span class="field-row-label">Estatística DW</span><span class="field-row-value">{diagnosticos["dw"]:.4f}</span></div>'''
|
| 153 |
+
html += f'''<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value">{diagnosticos["interp_DW"]}</span></div>'''
|
| 154 |
+
|
| 155 |
+
# Teste de Homocedasticidade (Breusch-Pagan)
|
| 156 |
+
html += '<div class="section-title-orange">Teste de Homocedasticidade (Breusch-Pagan)</div>'
|
| 157 |
+
html += f'''<div class="field-row"><span class="field-row-label">Estatística LM</span><span class="field-row-value">{diagnosticos["bp_lm"]:.4f}</span></div>'''
|
| 158 |
+
html += f'''<div class="field-row"><span class="field-row-label">P-valor</span><span class="field-row-value">{diagnosticos["bp_p"]:.4f}</span></div>'''
|
| 159 |
+
html += f'''<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value">{diagnosticos["interp_BP"]}</span></div>'''
|
| 160 |
+
|
| 161 |
+
# Equação do Modelo
|
| 162 |
+
html += '<div class="section-title-orange">Equação do Modelo</div>'
|
| 163 |
+
html += f'<div class="equation-box">{diagnosticos["equacao"]}</div>'
|
| 164 |
+
|
| 165 |
+
html += '</div>'
|
| 166 |
+
return html
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def formatar_busca_html(resultados_busca):
|
| 170 |
+
"""Formata resultados da busca automática como HTML, incluindo graus de enquadramento."""
|
| 171 |
+
if not resultados_busca:
|
| 172 |
+
return "<p>Execute a busca automática para ver resultados.</p>"
|
| 173 |
+
|
| 174 |
+
_GRAU_LABEL = {3: "Grau III", 2: "Grau II", 1: "Grau I", 0: "Sem enq."}
|
| 175 |
+
_GRAU_COR = {3: "#2e7d32", 2: "#1565c0", 1: "#e65100", 0: "#b71c1c"}
|
| 176 |
+
|
| 177 |
+
html = '<div class="busca-container">'
|
| 178 |
+
|
| 179 |
+
for r in resultados_busca:
|
| 180 |
+
graus_coef = r.get("graus_coef", {})
|
| 181 |
+
grau_f = r.get("grau_f", 0)
|
| 182 |
+
grau_f_label = _GRAU_LABEL.get(grau_f, "Sem enq.")
|
| 183 |
+
grau_f_cor = _GRAU_COR.get(grau_f, "#b71c1c")
|
| 184 |
+
|
| 185 |
+
html += f'''
|
| 186 |
+
<div class="modelo-card" style="border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin: 8px 0; background: #f9f9f9;">
|
| 187 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
| 188 |
+
<span class="modelo-rank" style="font-weight: bold; font-size: 1.1em;">#{r["rank"]}</span>
|
| 189 |
+
<span class="modelo-r2" style="background: #e3f2fd; padding: 4px 8px; border-radius: 4px;">R² = {r["r2"]:.4f}</span>
|
| 190 |
+
</div>
|
| 191 |
+
<div class="modelo-transf" style="font-size: 0.95em;">
|
| 192 |
+
<b>y:</b> {r["transformacao_y"]}<br>
|
| 193 |
+
'''
|
| 194 |
+
for col, transf in r["transformacoes_x"].items():
|
| 195 |
+
grau = graus_coef.get(col, 0)
|
| 196 |
+
label = _GRAU_LABEL.get(grau, "Sem enq.")
|
| 197 |
+
cor = _GRAU_COR.get(grau, "#b71c1c")
|
| 198 |
+
html += (f'<b>{col}:</b> {transf}'
|
| 199 |
+
f' — <span style="color:{cor}; font-weight:bold; font-size:0.85em;">{label}</span><br>')
|
| 200 |
+
|
| 201 |
+
html += f'''
|
| 202 |
+
</div>
|
| 203 |
+
<div style="margin-top: 6px; padding-top: 6px; border-top: 1px solid #e0e0e0; font-size: 0.85em;">
|
| 204 |
+
Teste F: <span style="color:{grau_f_cor}; font-weight:bold;">{grau_f_label}</span>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
'''
|
| 208 |
+
|
| 209 |
+
html += '</div>'
|
| 210 |
+
return html
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _renderizar_secao_micro(titulo, resultados_dict):
|
| 214 |
+
"""Renderiza uma seção de micronumerosidade (dicotômicas ou códigos alocados)."""
|
| 215 |
+
if not resultados_dict:
|
| 216 |
+
return ""
|
| 217 |
+
|
| 218 |
+
html = f'<div class="section-title-orange">{titulo}</div>'
|
| 219 |
+
html += '<div class="micro-grid">'
|
| 220 |
+
|
| 221 |
+
for coluna, info in resultados_dict.items():
|
| 222 |
+
status = "✅" if info["valido"] else "⚠️"
|
| 223 |
+
status_class = "micro-ok" if info["valido"] else "micro-warn"
|
| 224 |
+
mensagens_lista = info["mensagens"]
|
| 225 |
+
mensagens_html = "".join(f'<div class="micro-msg">{msg}</div>' for msg in mensagens_lista)
|
| 226 |
+
|
| 227 |
+
html += f'''
|
| 228 |
+
<div class="teste-item micro-card {status_class}">
|
| 229 |
+
<div class="micro-card-head">
|
| 230 |
+
<span class="teste-nome micro-title">{coluna}</span>
|
| 231 |
+
<span class="teste-valor micro-status">{status}</span>
|
| 232 |
+
</div>
|
| 233 |
+
<div class="micro-msg-grid">
|
| 234 |
+
{mensagens_html}
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
'''
|
| 238 |
+
|
| 239 |
+
html += '</div>'
|
| 240 |
+
return html
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def formatar_micronumerosidade_html(resultado):
|
| 244 |
+
"""Formata resultado do teste de micronumerosidade como HTML."""
|
| 245 |
+
|
| 246 |
+
if not resultado:
|
| 247 |
+
return "<p>Clique em 'Aplicar Seleção' para verificar micronumerosidade.</p>"
|
| 248 |
+
|
| 249 |
+
html = '<div class="diagnosticos-container">'
|
| 250 |
+
html += '<div class="section-title-orange">Teste de Micronumerosidade (NBR 14.653-2)</div>'
|
| 251 |
+
|
| 252 |
+
# =========================
|
| 253 |
+
# Condição Geral
|
| 254 |
+
# =========================
|
| 255 |
+
n, k = resultado["n"], resultado["k"]
|
| 256 |
+
condicao_ok = resultado["condicao_geral_ok"]
|
| 257 |
+
req = 3 * (k + 1)
|
| 258 |
+
|
| 259 |
+
status_geral = (
|
| 260 |
+
f"✅ n = {n} ≥ 3(k+1) = {req}"
|
| 261 |
+
if condicao_ok
|
| 262 |
+
else f"❌ n = {n} < 3(k+1) = {req}"
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
html += f'''
|
| 266 |
+
<div class="micro-summary-grid">
|
| 267 |
+
<div class="stat-item micro-summary-card">
|
| 268 |
+
<span class="stat-label">n (observações)</span>
|
| 269 |
+
<span class="stat-value micro-summary-value">{n}</span>
|
| 270 |
+
</div>
|
| 271 |
+
<div class="stat-item micro-summary-card">
|
| 272 |
+
<span class="stat-label">k (variáveis)</span>
|
| 273 |
+
<span class="stat-value micro-summary-value">{k}</span>
|
| 274 |
+
</div>
|
| 275 |
+
<div class="stat-item micro-summary-card micro-summary-wide">
|
| 276 |
+
<span class="stat-label">Condição geral</span>
|
| 277 |
+
<span class="stat-value micro-summary-status">{status_geral}</span>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
'''
|
| 281 |
+
|
| 282 |
+
# =========================
|
| 283 |
+
# Seções separadas: Dicotômicas e Códigos Alocados
|
| 284 |
+
# =========================
|
| 285 |
+
dic_html = _renderizar_secao_micro("Variáveis Dicotômicas", resultado.get("dicotomicas", {}))
|
| 286 |
+
cod_html = _renderizar_secao_micro("Códigos Ajustados/Alocados", resultado.get("codigo_alocado", {}))
|
| 287 |
+
|
| 288 |
+
if dic_html or cod_html:
|
| 289 |
+
html += dic_html + cod_html
|
| 290 |
+
else:
|
| 291 |
+
html += '''
|
| 292 |
+
<p style="color: #6c757d; font-style: italic; margin-top: 8px;">
|
| 293 |
+
Nenhuma variável dicotômica ou de código alocado selecionada.
|
| 294 |
+
</p>
|
| 295 |
+
'''
|
| 296 |
+
|
| 297 |
+
html += '</div>'
|
| 298 |
+
return html
|
| 299 |
+
|
| 300 |
+
def formatar_outliers_anteriores_html(n_outliers, lista_indices):
|
| 301 |
+
"""Formata informações de outliers excluídos como HTML card."""
|
| 302 |
+
lista_str = lista_indices if lista_indices else "Nenhum"
|
| 303 |
+
return f'''
|
| 304 |
+
<div style="display: flex; gap: 16px; align-items: baseline;
|
| 305 |
+
padding: 10px 16px; background: var(--background-fill-secondary, #f8f9fa);
|
| 306 |
+
border-radius: 8px; border: 1px solid var(--border-color-primary, #e2e8f0);
|
| 307 |
+
flex-wrap: wrap;">
|
| 308 |
+
<span style="font-weight: 600; color: var(--body-text-color, #495057); white-space: nowrap;">
|
| 309 |
+
{n_outliers} outlier(s) excluídos do modelo ajustado
|
| 310 |
+
</span>
|
| 311 |
+
<span style="color: var(--body-text-color-subdued, #6c757d); font-size: 0.92em;">
|
| 312 |
+
Índices: {lista_str}
|
| 313 |
+
</span>
|
| 314 |
+
</div>'''
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ============================================================
|
| 318 |
+
# AVALIAÇÃO DE IMÓVEL
|
| 319 |
+
# ============================================================
|
| 320 |
+
|
| 321 |
+
_CORES_GRAU = {
|
| 322 |
+
"Grau III": "#28a745",
|
| 323 |
+
"Grau II": "#17a2b8",
|
| 324 |
+
"Grau I": "#e67e00",
|
| 325 |
+
"Sem enquadramento": "#dc3545",
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def _formatar_brl(valor):
|
| 330 |
+
"""Formata n��mero no padrão brasileiro: R$ 350.000,00."""
|
| 331 |
+
if valor is None or (isinstance(valor, float) and np.isnan(valor)):
|
| 332 |
+
return "—"
|
| 333 |
+
s = f"{valor:,.2f}"
|
| 334 |
+
s = s.replace(",", "X").replace(".", ",").replace("X", ".")
|
| 335 |
+
return f"R$ {s}"
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def _popup_grau(conteudo_html):
|
| 339 |
+
"""Gera ícone ⓘ com popup estilizado no hover.
|
| 340 |
+
|
| 341 |
+
Args:
|
| 342 |
+
conteudo_html: HTML interno do popup.
|
| 343 |
+
|
| 344 |
+
Returns:
|
| 345 |
+
str HTML do ícone + popup.
|
| 346 |
+
"""
|
| 347 |
+
return (
|
| 348 |
+
'<span style="position: relative; display: inline-block; cursor: help;" '
|
| 349 |
+
'class="mesa-popup-wrap">'
|
| 350 |
+
'<span style="font-size: 0.85em; opacity: 0.7;">ⓘ</span>'
|
| 351 |
+
'<span class="mesa-popup-content" style="'
|
| 352 |
+
'display: none; position: absolute; bottom: calc(100% + 8px); right: 0;'
|
| 353 |
+
' z-index: 1000; width: 520px;'
|
| 354 |
+
' background: #fff; border: 1px solid #dee2e6; border-radius: 8px;'
|
| 355 |
+
' box-shadow: 0 4px 16px rgba(0,0,0,0.15); padding: 10px 14px;'
|
| 356 |
+
' font-size: 12px; font-weight: 400; color: #333; text-align: left;'
|
| 357 |
+
' line-height: 1.4; white-space: normal;">'
|
| 358 |
+
f'{conteudo_html}'
|
| 359 |
+
'</span>'
|
| 360 |
+
'</span>'
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def _popup_precisao_html(aval):
|
| 365 |
+
"""Gera conteúdo HTML do popup para o grau de Precisão."""
|
| 366 |
+
amplitude = aval["amplitude"]
|
| 367 |
+
grau = aval["precisao"]
|
| 368 |
+
|
| 369 |
+
regras = [
|
| 370 |
+
("≤30%", "Grau III", "#28a745"),
|
| 371 |
+
("≤40%", "Grau II", "#17a2b8"),
|
| 372 |
+
("≤50%", "Grau I", "#e67e00"),
|
| 373 |
+
(">50%", "Sem enquadramento", "#dc3545"),
|
| 374 |
+
]
|
| 375 |
+
|
| 376 |
+
html = (
|
| 377 |
+
'<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">'
|
| 378 |
+
'Precisão — NBR 14.653-2, Tabela 2'
|
| 379 |
+
'<span style="font-weight: 400; font-size: 11px; color: #6c757d; margin-left: 8px;">'
|
| 380 |
+
'Amplitude do IC 80% em relação ao estimado.</span></div>'
|
| 381 |
+
'<div style="display: flex; align-items: baseline; gap: 12px; margin-bottom: 6px;">'
|
| 382 |
+
f'<span>Amplitude do IC 80%: <b>{amplitude:.1f}%</b></span>'
|
| 383 |
+
'</div>'
|
| 384 |
+
'<table style="width: 100%; border-collapse: collapse; font-size: 11px;">'
|
| 385 |
+
'<tr style="border-bottom: 1px solid #e9ecef;">'
|
| 386 |
+
'<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Amplitude</th>'
|
| 387 |
+
'<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Grau</th>'
|
| 388 |
+
'</tr>'
|
| 389 |
+
)
|
| 390 |
+
for faixa, nome, cor in regras:
|
| 391 |
+
ativo = (nome == grau)
|
| 392 |
+
bg = f'background: {cor}11;' if ativo else ''
|
| 393 |
+
fw = 'font-weight: 600;' if ativo else ''
|
| 394 |
+
marca = ' ◀' if ativo else ''
|
| 395 |
+
html += (
|
| 396 |
+
f'<tr style="border-bottom: 1px solid #f0f0f0; {bg}">'
|
| 397 |
+
f'<td style="padding: 2px 6px; {fw}">{faixa}</td>'
|
| 398 |
+
f'<td style="padding: 2px 6px; color: {cor}; {fw}">{nome}{marca}</td>'
|
| 399 |
+
'</tr>'
|
| 400 |
+
)
|
| 401 |
+
html += '</table>'
|
| 402 |
+
return html
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def _popup_fundamentacao_html(aval):
|
| 406 |
+
"""Gera conteúdo HTML do popup para o grau de Fundamentação."""
|
| 407 |
+
grau = aval["fundamentacao"]
|
| 408 |
+
cor_grau = _CORES_GRAU.get(grau, "#495057")
|
| 409 |
+
|
| 410 |
+
_header = (
|
| 411 |
+
'<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">'
|
| 412 |
+
'Fundamentação — NBR 14.653-2, Tabela 1</div>'
|
| 413 |
+
'<div style="font-size: 11px; color: #6c757d; margin-bottom: 6px; line-height: 1.45;">'
|
| 414 |
+
'Depende de quantas variáveis extrapolam os limites amostrais e do '
|
| 415 |
+
'<b style="color:#333;">impacto no valor estimado</b>. '
|
| 416 |
+
'O impacto é calculado aplicando o modelo duas vezes: uma com os valores informados '
|
| 417 |
+
'(incluindo os extrapolados) e outra simulando as variáveis extrapoladas no seu limite '
|
| 418 |
+
'amostral mais próximo (mínimo ou máximo, conforme o caso). A diferença percentual entre '
|
| 419 |
+
'os dois valores unitários resultantes é o impacto no estimado. '
|
| 420 |
+
'É esse impacto — e não a % de extrapolação individual de cada variável — que define o grau, '
|
| 421 |
+
'exceto no caso de extrapolação grave'
|
| 422 |
+
' (variável >100% acima do máximo amostral, >50% abaixo do mínimo amostral, ou valor '
|
| 423 |
+
'inválido em dicotômica), que resulta automaticamente em Sem enquadramento.'
|
| 424 |
+
'</div>'
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
if not aval["houve_extrapolacao"]:
|
| 428 |
+
return (
|
| 429 |
+
f'{_header}'
|
| 430 |
+
'<div>Nenhuma variável extrapolou os limites amostrais. '
|
| 431 |
+
f'<span style="color: #28a745; font-weight: 600;">→ {grau}</span></div>'
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
n = aval["qtd_extrapolacoes"]
|
| 435 |
+
perc = aval.get("perc_ext", 0) or 0
|
| 436 |
+
tem_grave = any(
|
| 437 |
+
info["status"] == "grave"
|
| 438 |
+
for info in aval["extrapolacoes"].values()
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
# Motivo conciso
|
| 442 |
+
if tem_grave:
|
| 443 |
+
motivo = "Extrapolação grave (>100% do máx., >50% abaixo do mín., ou dicotômica inválida)."
|
| 444 |
+
elif perc > 20:
|
| 445 |
+
motivo = f"Impacto de {perc:.1f}% no estimado, acima do limite de 20%."
|
| 446 |
+
elif n >= 2:
|
| 447 |
+
motivo = f"{n} variáveis extrapoladas, impacto de {perc:.1f}% no estimado."
|
| 448 |
+
elif n == 1 and perc > 15:
|
| 449 |
+
motivo = f"1 variável extrapolada, impacto de {perc:.1f}% no estimado (>15%)."
|
| 450 |
+
else:
|
| 451 |
+
motivo = f"1 variável extrapolada, impacto de {perc:.1f}% no estimado (≤15%)."
|
| 452 |
+
|
| 453 |
+
# Variáveis extrapoladas inline
|
| 454 |
+
vars_items = []
|
| 455 |
+
for col, info in aval["extrapolacoes"].items():
|
| 456 |
+
st = info.get("status", "ok")
|
| 457 |
+
if st in ("warning", "grave"):
|
| 458 |
+
p = info.get("percentual", 0)
|
| 459 |
+
direcao = info.get("direcao", "")
|
| 460 |
+
icone = '⚠️' if st == "warning" else '❌'
|
| 461 |
+
if direcao == "acima":
|
| 462 |
+
desc = f'{p:.1f}% acima do máx.'
|
| 463 |
+
elif direcao == "abaixo":
|
| 464 |
+
desc = f'{p:.1f}% abaixo do mín.'
|
| 465 |
+
elif info.get("valor_invalido"):
|
| 466 |
+
desc = 'valor inválido (dicot.)'
|
| 467 |
+
else:
|
| 468 |
+
desc = f'{p:.1f}% fora'
|
| 469 |
+
vars_items.append(f'{icone} <b>{col}</b>: {desc}')
|
| 470 |
+
|
| 471 |
+
# Regras
|
| 472 |
+
regras = [
|
| 473 |
+
("Nenhuma extrapolação", "Grau III"),
|
| 474 |
+
("1 variável extrapolada, impacto ≤15% no estimado", "Grau II"),
|
| 475 |
+
("1 variável extrapolada c/ impacto >15% e ≤20%, ou >1 variável extrapolada c/ impacto ≤20%", "Grau I"),
|
| 476 |
+
("Impacto >20% no estimado, ou extrapolação grave", "Sem enq."),
|
| 477 |
+
]
|
| 478 |
+
|
| 479 |
+
html = _header
|
| 480 |
+
|
| 481 |
+
# Resumo + variáveis numa linha compacta
|
| 482 |
+
resumo = f'{n} variável(is) extrapolada(s)'
|
| 483 |
+
if perc is not None and perc > 0:
|
| 484 |
+
resumo += f', impacto de <b>{perc:.1f}%</b> no estimado'
|
| 485 |
+
html += f'<div style="margin-bottom: 3px;">{resumo}.</div>'
|
| 486 |
+
|
| 487 |
+
if vars_items:
|
| 488 |
+
vars_str = ' │ '.join(vars_items)
|
| 489 |
+
html += (
|
| 490 |
+
f'<div style="background: #f8f9fa; border-radius: 4px; padding: 4px 8px;'
|
| 491 |
+
f' margin-bottom: 4px; font-size: 11px;">{vars_str}</div>'
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
html += (
|
| 495 |
+
f'<div style="margin-bottom: 4px; font-size: 11px; color: #495057;">'
|
| 496 |
+
f'{motivo} <span style="color: {cor_grau}; font-weight: 600;">→ {grau}</span></div>'
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
# Tabela compacta
|
| 500 |
+
html += (
|
| 501 |
+
'<table style="width: 100%; border-collapse: collapse; font-size: 11px;">'
|
| 502 |
+
'<tr style="border-bottom: 1px solid #e9ecef;">'
|
| 503 |
+
'<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Condição</th>'
|
| 504 |
+
'<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Grau</th>'
|
| 505 |
+
'</tr>'
|
| 506 |
+
)
|
| 507 |
+
for cond, nome in regras:
|
| 508 |
+
ativo = (nome == grau) or (nome == "Sem enq." and grau == "Sem enquadramento")
|
| 509 |
+
bg = f'background: {cor_grau}11;' if ativo else ''
|
| 510 |
+
fw = 'font-weight: 600;' if ativo else ''
|
| 511 |
+
marca = ' ◀' if ativo else ''
|
| 512 |
+
html += (
|
| 513 |
+
f'<tr style="border-bottom: 1px solid #f0f0f0; {bg}">'
|
| 514 |
+
f'<td style="padding: 2px 6px; {fw}">{cond}</td>'
|
| 515 |
+
f'<td style="padding: 2px 6px; {fw}">{nome}{marca}</td>'
|
| 516 |
+
'</tr>'
|
| 517 |
+
)
|
| 518 |
+
html += '</table>'
|
| 519 |
+
return html
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def formatar_avaliacao_html(avaliacoes_lista, indice_base=0, elem_id_excluir="excluir-aval-elab"):
|
| 523 |
+
"""Formata resultados de avaliação como tabela HTML acumulada.
|
| 524 |
+
|
| 525 |
+
Mostra tabela desde a 1ª avaliação. Cada coluna = 1 avaliação.
|
| 526 |
+
Extrapolação com % ao lado do ícone. Tooltips nos graus.
|
| 527 |
+
Linha "Estimado / Base" compara cada avaliação com a base.
|
| 528 |
+
Linha "Excluir" com ícone de lixeira por coluna.
|
| 529 |
+
|
| 530 |
+
Args:
|
| 531 |
+
avaliacoes_lista: lista de dicts retornados por core.avaliar_imovel().
|
| 532 |
+
indice_base: índice (0-based) da avaliação base para comparação.
|
| 533 |
+
elem_id_excluir: elem_id do Textbox hidden que recebe o índice a excluir.
|
| 534 |
+
|
| 535 |
+
Returns:
|
| 536 |
+
str HTML.
|
| 537 |
+
"""
|
| 538 |
+
if not avaliacoes_lista:
|
| 539 |
+
return ""
|
| 540 |
+
|
| 541 |
+
n = len(avaliacoes_lista)
|
| 542 |
+
|
| 543 |
+
# Corrigir índice base se fora de range
|
| 544 |
+
if indice_base < 0 or indice_base >= n:
|
| 545 |
+
indice_base = 0
|
| 546 |
+
|
| 547 |
+
# Estilos reutilizáveis
|
| 548 |
+
_td = 'style="padding: 6px 12px; border-bottom: 1px solid #f0f0f0;"'
|
| 549 |
+
_td_r = 'style="text-align: right; padding: 6px 12px; border-bottom: 1px solid #f0f0f0;"'
|
| 550 |
+
_td_bold = 'style="padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-weight: 600;"'
|
| 551 |
+
_td_bold_r = 'style="text-align: right; padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-weight: 600;"'
|
| 552 |
+
|
| 553 |
+
html = (
|
| 554 |
+
'<style>'
|
| 555 |
+
'.mesa-popup-wrap:hover .mesa-popup-content { display: block !important; }'
|
| 556 |
+
'</style>'
|
| 557 |
+
'<div style="overflow-x: auto;">'
|
| 558 |
+
)
|
| 559 |
+
html += '<table style="width: 100%; border-collapse: collapse; font-size: 13px;">'
|
| 560 |
+
|
| 561 |
+
# Header
|
| 562 |
+
html += '<tr style="background: #f8f9fa;">'
|
| 563 |
+
html += '<th style="text-align: left; padding: 8px 12px; border-bottom: 2px solid #dee2e6; font-weight: 600; color: #495057;"></th>'
|
| 564 |
+
for i in range(n):
|
| 565 |
+
html += f'<th style="text-align: right; padding: 8px 12px; border-bottom: 2px solid #dee2e6; font-weight: 600; color: #495057;">Aval. {i+1}</th>'
|
| 566 |
+
html += '</tr>'
|
| 567 |
+
|
| 568 |
+
# Variáveis X
|
| 569 |
+
colunas_x = list(avaliacoes_lista[0]["valores_x"].keys())
|
| 570 |
+
for col in colunas_x:
|
| 571 |
+
html += '<tr>'
|
| 572 |
+
html += f'<td {_td} style="padding: 6px 12px; border-bottom: 1px solid #f0f0f0; font-weight: 500; color: #495057;">{col}</td>'
|
| 573 |
+
for aval in avaliacoes_lista:
|
| 574 |
+
val = aval["valores_x"].get(col, 0)
|
| 575 |
+
ext = aval["extrapolacoes"].get(col, {})
|
| 576 |
+
status = ext.get("status", "ok")
|
| 577 |
+
perc = ext.get("percentual", 0)
|
| 578 |
+
val_fmt = f"{val:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 579 |
+
|
| 580 |
+
if status == "ok":
|
| 581 |
+
celula = f'{val_fmt} ✅'
|
| 582 |
+
elif status in ("dicotomica", "codigo_alocado", "percentual"):
|
| 583 |
+
celula = f'{val_fmt} \u2014'
|
| 584 |
+
elif status == "warning":
|
| 585 |
+
celula = f'{val_fmt} \u26a0\ufe0f {perc:.1f}%'
|
| 586 |
+
elif ext.get("valor_invalido"):
|
| 587 |
+
celula = f'{val_fmt} \u274c'
|
| 588 |
+
else: # grave
|
| 589 |
+
celula = f'{val_fmt} \u274c {perc:.1f}%'
|
| 590 |
+
|
| 591 |
+
html += f'<td {_td_r}>{celula}</td>'
|
| 592 |
+
html += '</tr>'
|
| 593 |
+
|
| 594 |
+
# Estimado
|
| 595 |
+
html += '<tr style="background: #f8f9fa; font-weight: 600;">'
|
| 596 |
+
html += f'<td {_td_bold}>Estimado</td>'
|
| 597 |
+
for aval in avaliacoes_lista:
|
| 598 |
+
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>'
|
| 599 |
+
html += '</tr>'
|
| 600 |
+
|
| 601 |
+
# Estimado / Base (só se houver mais de 1 avaliação)
|
| 602 |
+
if n > 1:
|
| 603 |
+
estimado_base = avaliacoes_lista[indice_base]["estimado"]
|
| 604 |
+
html += '<tr style="background: #fff8f0; font-size: 12px;">'
|
| 605 |
+
html += f'<td style="padding: 4px 12px; border-bottom: 1px solid #f0f0f0; font-weight: 500; color: #6c757d; font-style: italic;">Estimado / Base (Aval. {indice_base + 1})</td>'
|
| 606 |
+
for i, aval in enumerate(avaliacoes_lista):
|
| 607 |
+
if i == indice_base:
|
| 608 |
+
celula = '<span style="color: #FF8C00; font-weight: 600;">Base</span>'
|
| 609 |
+
else:
|
| 610 |
+
if estimado_base != 0:
|
| 611 |
+
diff_perc = ((aval["estimado"] / estimado_base) - 1) * 100
|
| 612 |
+
sinal = "+" if diff_perc >= 0 else "\u2212"
|
| 613 |
+
cor = "#28a745" if diff_perc >= 0 else "#dc3545"
|
| 614 |
+
celula = f'<span style="color: {cor}; font-weight: 600;">{sinal}{abs(diff_perc):.1f}%</span>'
|
| 615 |
+
else:
|
| 616 |
+
celula = '\u2014'
|
| 617 |
+
html += f'<td style="text-align: right; padding: 4px 12px; border-bottom: 1px solid #f0f0f0;">{celula}</td>'
|
| 618 |
+
html += '</tr>'
|
| 619 |
+
|
| 620 |
+
# CA
|
| 621 |
+
html += f'<tr><td {_td}>CA \u221215%</td>'
|
| 622 |
+
for aval in avaliacoes_lista:
|
| 623 |
+
html += f'<td {_td_r}>{_formatar_brl(aval["ca_inf"])}</td>'
|
| 624 |
+
html += '</tr>'
|
| 625 |
+
html += f'<tr><td {_td}>CA +15%</td>'
|
| 626 |
+
for aval in avaliacoes_lista:
|
| 627 |
+
html += f'<td {_td_r}>{_formatar_brl(aval["ca_sup"])}</td>'
|
| 628 |
+
html += '</tr>'
|
| 629 |
+
|
| 630 |
+
# IC
|
| 631 |
+
html += f'<tr><td {_td}>IC 80% Inf.</td>'
|
| 632 |
+
for aval in avaliacoes_lista:
|
| 633 |
+
html += f'<td {_td_r}>{_formatar_brl(aval["ic_inf"])} (\u2212{aval["perc_inf"]:.1f}%)</td>'
|
| 634 |
+
html += '</tr>'
|
| 635 |
+
html += f'<tr><td {_td}>IC 80% Sup.</td>'
|
| 636 |
+
for aval in avaliacoes_lista:
|
| 637 |
+
html += f'<td {_td_r}>{_formatar_brl(aval["ic_sup"])} (+{aval["perc_sup"]:.1f}%)</td>'
|
| 638 |
+
html += '</tr>'
|
| 639 |
+
|
| 640 |
+
# Amplitude
|
| 641 |
+
html += f'<tr><td {_td}>Amplitude</td>'
|
| 642 |
+
for aval in avaliacoes_lista:
|
| 643 |
+
html += f'<td {_td_r}>{aval["amplitude"]:.1f}%</td>'
|
| 644 |
+
html += '</tr>'
|
| 645 |
+
|
| 646 |
+
# Precisão (negrito, cor, popup)
|
| 647 |
+
html += '<tr style="background: #f8f9fa;">'
|
| 648 |
+
html += f'<td {_td_bold}>Precisão</td>'
|
| 649 |
+
for aval in avaliacoes_lista:
|
| 650 |
+
cor = _CORES_GRAU.get(aval["precisao"], "#495057")
|
| 651 |
+
popup = _popup_grau(_popup_precisao_html(aval))
|
| 652 |
+
html += (
|
| 653 |
+
f'<td {_td_bold_r} style="text-align: right; padding: 8px 12px; '
|
| 654 |
+
f'border-bottom: 1px solid #dee2e6; color: {cor}; font-weight: 600;">'
|
| 655 |
+
f'{aval["precisao"]} {popup}'
|
| 656 |
+
f'</td>'
|
| 657 |
+
)
|
| 658 |
+
html += '</tr>'
|
| 659 |
+
|
| 660 |
+
# Fundamentação (negrito, cor, popup)
|
| 661 |
+
html += '<tr style="background: #f8f9fa;">'
|
| 662 |
+
html += f'<td {_td_bold}>Fundamentação</td>'
|
| 663 |
+
for aval in avaliacoes_lista:
|
| 664 |
+
cor = _CORES_GRAU.get(aval["fundamentacao"], "#495057")
|
| 665 |
+
popup = _popup_grau(_popup_fundamentacao_html(aval))
|
| 666 |
+
html += (
|
| 667 |
+
f'<td {_td_bold_r} style="text-align: right; padding: 8px 12px; '
|
| 668 |
+
f'border-bottom: 1px solid #dee2e6; color: {cor}; font-weight: 600;">'
|
| 669 |
+
f'{aval["fundamentacao"]} {popup}'
|
| 670 |
+
f'</td>'
|
| 671 |
+
)
|
| 672 |
+
html += '</tr>'
|
| 673 |
+
|
| 674 |
+
# Excluir (linha com lixeiras → botão vermelho temporário)
|
| 675 |
+
# JS inline em cada onclick (scripts via innerHTML não são executados)
|
| 676 |
+
_js_trigger = (
|
| 677 |
+
"var el=document.querySelector('#{eid} textarea')"
|
| 678 |
+
"||document.querySelector('#{eid} input');"
|
| 679 |
+
"if(el){{el.value=String({idx});"
|
| 680 |
+
"el.dispatchEvent(new Event('input',{{bubbles:true}}));"
|
| 681 |
+
"el.dispatchEvent(new Event('change',{{bubbles:true}}));}}"
|
| 682 |
+
)
|
| 683 |
+
html += '<tr style="background: #fff5f5;">'
|
| 684 |
+
html += f'<td style="padding: 6px 12px; border-bottom: 1px solid #f0f0f0; font-weight: 500; color: #dc3545; font-size: 12px;">Excluir</td>'
|
| 685 |
+
for i in range(n):
|
| 686 |
+
idx_1 = i + 1
|
| 687 |
+
uid = f'{elem_id_excluir}-{idx_1}'
|
| 688 |
+
# Clicar lixeira → mostra botão vermelho por 10s
|
| 689 |
+
onclick_trash = (
|
| 690 |
+
f"document.getElementById('{uid}-trash').style.display='none';"
|
| 691 |
+
f"document.getElementById('{uid}-btn').style.display='inline-block';"
|
| 692 |
+
f"clearTimeout(window['_t_{uid}']);"
|
| 693 |
+
f"window['_t_{uid}']=setTimeout(function(){{"
|
| 694 |
+
f"document.getElementById('{uid}-btn').style.display='none';"
|
| 695 |
+
f"document.getElementById('{uid}-trash').style.display='inline';"
|
| 696 |
+
f"}},10000);"
|
| 697 |
+
)
|
| 698 |
+
# Clicar botão vermelho → dispara exclusão inline
|
| 699 |
+
onclick_btn = (
|
| 700 |
+
f"clearTimeout(window['_t_{uid}']);"
|
| 701 |
+
+ _js_trigger.format(eid=elem_id_excluir, idx=idx_1)
|
| 702 |
+
)
|
| 703 |
+
html += (
|
| 704 |
+
f'<td style="text-align: right; padding: 6px 12px; border-bottom: 1px solid #f0f0f0;">'
|
| 705 |
+
f'<span id="{uid}-trash" onclick="{onclick_trash}" '
|
| 706 |
+
f'style="cursor: pointer; color: #dc3545; font-size: 18px;" '
|
| 707 |
+
f'title="Excluir Avaliação {idx_1}">'
|
| 708 |
+
f'\U0001f5d1\ufe0f</span>'
|
| 709 |
+
f'<span id="{uid}-btn" onclick="{onclick_btn}" '
|
| 710 |
+
f'style="display:none; cursor:pointer; background:#dc3545; color:white; '
|
| 711 |
+
f'padding:2px 10px; border-radius:4px; font-size:12px; font-weight:600;">'
|
| 712 |
+
f'Excluir</span>'
|
| 713 |
+
f'</td>'
|
| 714 |
+
)
|
| 715 |
+
html += '</tr>'
|
| 716 |
+
|
| 717 |
+
html += '</table></div>'
|
| 718 |
+
|
| 719 |
+
return html
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
def formatar_aviso_multicolinearidade(resultado):
|
| 723 |
+
"""Formata HTML de aviso de multicolinearidade para exibição na seção 4.
|
| 724 |
+
|
| 725 |
+
Args:
|
| 726 |
+
resultado: dict retornado por core.verificar_multicolinearidade, ou None
|
| 727 |
+
|
| 728 |
+
Returns:
|
| 729 |
+
tuple (html: str, visible: bool)
|
| 730 |
+
"""
|
| 731 |
+
if not resultado or (not resultado.get('perfeita') and not resultado.get('alta')):
|
| 732 |
+
return "", False
|
| 733 |
+
|
| 734 |
+
if resultado.get('perfeita'):
|
| 735 |
+
posto = resultado.get('posto', '?')
|
| 736 |
+
ncolunas = resultado.get('ncolunas', '?')
|
| 737 |
+
html = (
|
| 738 |
+
'<div style="margin-top:12px; padding:12px 16px; background:#fdecea; '
|
| 739 |
+
'border-left:4px solid #c0392b; border-radius:6px; color:#7b1a1a;">'
|
| 740 |
+
'<strong>⛔ Multicolinearidade Perfeita Detectada</strong><br>'
|
| 741 |
+
f'<span style="font-size:0.93em;">A matriz de regressoras tem dependência linear exata entre variáveis '
|
| 742 |
+
f'(posto {posto} < {ncolunas} colunas). '
|
| 743 |
+
'O modelo OLS <strong>não poderá ser estimado</strong> com as variáveis selecionadas. '
|
| 744 |
+
'Verifique se há dummies em excesso, variáveis redundantes ou combinações lineares exatas.</span>'
|
| 745 |
+
'</div>'
|
| 746 |
+
)
|
| 747 |
+
return html, True
|
| 748 |
+
|
| 749 |
+
# Alta (VIF > 10)
|
| 750 |
+
vif = resultado.get('vif', {})
|
| 751 |
+
vars_alta = resultado.get('vars_alta', [])
|
| 752 |
+
itens = ", ".join(
|
| 753 |
+
f"<strong>{c}</strong> (VIF={vif[c]:.1f})" for c in vars_alta if c in vif
|
| 754 |
+
)
|
| 755 |
+
html = (
|
| 756 |
+
'<div style="margin-top:12px; padding:12px 16px; background:#fff8e1; '
|
| 757 |
+
'border-left:4px solid #f39c12; border-radius:6px; color:#7d5a00;">'
|
| 758 |
+
'<strong>⚠️ Multicolinearidade Alta Detectada</strong><br>'
|
| 759 |
+
f'<span style="font-size:0.93em;">Variáveis com VIF > 10: {itens}. '
|
| 760 |
+
'A alta colinearidade pode causar instabilidade nos coeficientes estimados. '
|
| 761 |
+
'Considere remover variáveis redundantes antes da estimação.</span>'
|
| 762 |
+
'</div>'
|
| 763 |
+
)
|
| 764 |
+
return html, True
|
backend/app/core/elaboracao/geocodificacao.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
geocodificacao.py - Verificação, padronização e geocodificação de coordenadas geográficas.
|
| 4 |
+
|
| 5 |
+
Integrado na Seção 1 do MESA: verifica se lat/lon existem no DataFrame carregado
|
| 6 |
+
e, caso contrário, oferece geocodificação por interpolação em eixos de logradouros
|
| 7 |
+
(shapefile EixosLogradouros - Porto Alegre/RS - SIRGAS 2000 → EPSG:4326).
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
from .core import NOMES_LAT, NOMES_LON
|
| 14 |
+
|
| 15 |
+
# ============================================================
|
| 16 |
+
# CAMINHO DO SHAPEFILE
|
| 17 |
+
# ============================================================
|
| 18 |
+
|
| 19 |
+
_BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # raiz MESA/
|
| 20 |
+
_SHAPEFILE = os.path.join(_BASE, "dados", "EixosLogradouros.shp")
|
| 21 |
+
|
| 22 |
+
# Cache em módulo — carregado uma vez por sessão
|
| 23 |
+
_gdf_eixos = None
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def carregar_eixos():
|
| 27 |
+
"""Carrega e cacheia o GeoDataFrame dos eixos de logradouros.
|
| 28 |
+
|
| 29 |
+
Retorna None e registra aviso se geopandas/fiona não estiverem disponíveis
|
| 30 |
+
ou se o shapefile não for encontrado.
|
| 31 |
+
"""
|
| 32 |
+
global _gdf_eixos
|
| 33 |
+
if _gdf_eixos is not None:
|
| 34 |
+
return _gdf_eixos
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
import geopandas as gpd
|
| 38 |
+
except ImportError:
|
| 39 |
+
print("[geocodificacao] AVISO: geopandas não instalado — geocodificação indisponível.")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
if not os.path.exists(_SHAPEFILE):
|
| 43 |
+
print(f"[geocodificacao] AVISO: shapefile não encontrado em {_SHAPEFILE}")
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
gdf = gpd.read_file(_SHAPEFILE, engine="fiona")
|
| 47 |
+
gdf = gdf.to_crs("EPSG:4326")
|
| 48 |
+
_gdf_eixos = gdf
|
| 49 |
+
return _gdf_eixos
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ============================================================
|
| 53 |
+
# VERIFICAÇÃO E PADRONIZAÇÃO DE COORDENADAS
|
| 54 |
+
# ============================================================
|
| 55 |
+
|
| 56 |
+
def verificar_coords(df):
|
| 57 |
+
"""Verifica se o DataFrame possui colunas de lat e lon com ao menos 1 valor não-nulo.
|
| 58 |
+
|
| 59 |
+
Reutiliza NOMES_LAT / NOMES_LON de core.py (busca case-insensitive).
|
| 60 |
+
|
| 61 |
+
Retorna:
|
| 62 |
+
(True, col_lat, col_lon) — se encontradas e válidas
|
| 63 |
+
(False, None, None) — caso contrário
|
| 64 |
+
"""
|
| 65 |
+
colunas_lower = {str(col).lower(): col for col in df.columns}
|
| 66 |
+
|
| 67 |
+
col_lat = None
|
| 68 |
+
for nome in NOMES_LAT:
|
| 69 |
+
if nome in colunas_lower:
|
| 70 |
+
col_lat = colunas_lower[nome]
|
| 71 |
+
break
|
| 72 |
+
|
| 73 |
+
col_lon = None
|
| 74 |
+
for nome in NOMES_LON:
|
| 75 |
+
if nome in colunas_lower:
|
| 76 |
+
col_lon = colunas_lower[nome]
|
| 77 |
+
break
|
| 78 |
+
|
| 79 |
+
if col_lat is None or col_lon is None:
|
| 80 |
+
return False, None, None
|
| 81 |
+
|
| 82 |
+
# Verifica se há ao menos 1 valor não-nulo em cada coluna
|
| 83 |
+
lat_ok = pd.to_numeric(df[col_lat], errors="coerce").notna().any()
|
| 84 |
+
lon_ok = pd.to_numeric(df[col_lon], errors="coerce").notna().any()
|
| 85 |
+
|
| 86 |
+
if lat_ok and lon_ok:
|
| 87 |
+
return True, col_lat, col_lon
|
| 88 |
+
|
| 89 |
+
return False, None, None
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def padronizar_coords(df, col_lat, col_lon):
|
| 93 |
+
"""Cria (ou mantém) as colunas 'lat' e 'lon' no DataFrame.
|
| 94 |
+
|
| 95 |
+
Se col_lat/col_lon já são 'lat'/'lon', não faz nada além de garantir
|
| 96 |
+
que os valores são numéricos. Caso contrário, copia os valores
|
| 97 |
+
e remove as colunas originais.
|
| 98 |
+
|
| 99 |
+
Retorna:
|
| 100 |
+
DataFrame com colunas 'lat' e 'lon' padronizadas.
|
| 101 |
+
"""
|
| 102 |
+
df = df.copy()
|
| 103 |
+
|
| 104 |
+
if col_lat != "lat":
|
| 105 |
+
df["lat"] = pd.to_numeric(df[col_lat], errors="coerce")
|
| 106 |
+
df = df.drop(columns=[col_lat])
|
| 107 |
+
else:
|
| 108 |
+
df["lat"] = pd.to_numeric(df["lat"], errors="coerce")
|
| 109 |
+
|
| 110 |
+
if col_lon != "lon":
|
| 111 |
+
df["lon"] = pd.to_numeric(df[col_lon], errors="coerce")
|
| 112 |
+
df = df.drop(columns=[col_lon])
|
| 113 |
+
else:
|
| 114 |
+
df["lon"] = pd.to_numeric(df["lon"], errors="coerce")
|
| 115 |
+
|
| 116 |
+
return df
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ============================================================
|
| 120 |
+
# AUTO-DETECÇÃO DE COLUNAS PARA GEOCODIFICAÇÃO
|
| 121 |
+
# ============================================================
|
| 122 |
+
|
| 123 |
+
def auto_detectar_colunas_geo(df):
|
| 124 |
+
"""Tenta identificar automaticamente as colunas de CDLOG e número predial.
|
| 125 |
+
|
| 126 |
+
Retorna:
|
| 127 |
+
(col_cdlog, col_num) — strings ou None se não detectado
|
| 128 |
+
"""
|
| 129 |
+
colunas_upper = {str(c).upper(): c for c in df.columns}
|
| 130 |
+
|
| 131 |
+
# CDLOG
|
| 132 |
+
col_cdlog = colunas_upper.get("CDLOG") or colunas_upper.get("CTM")
|
| 133 |
+
|
| 134 |
+
# Número predial — em ordem de prioridade
|
| 135 |
+
col_num = None
|
| 136 |
+
for nome in ["Nº GEO", "NUM_GEO", "NUM", "NUMERO"]:
|
| 137 |
+
if nome in colunas_upper:
|
| 138 |
+
col_num = colunas_upper[nome]
|
| 139 |
+
break
|
| 140 |
+
|
| 141 |
+
return col_cdlog, col_num
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# ============================================================
|
| 145 |
+
# GEOCODIFICAÇÃO POR INTERPOLAÇÃO EM EIXOS
|
| 146 |
+
# ============================================================
|
| 147 |
+
|
| 148 |
+
def geocodificar(df, col_cdlog, col_num, auto_200=False):
|
| 149 |
+
"""Geocodifica registros do DataFrame usando interpolação em eixos de logradouros.
|
| 150 |
+
|
| 151 |
+
Para cada linha:
|
| 152 |
+
1. Localiza segmentos do logradouro pelo CDLOG no shapefile
|
| 153 |
+
2. Determina lado par/ímpar do número predial
|
| 154 |
+
3. Encontra segmento cujo intervalo contenha o número
|
| 155 |
+
4. Se encontrado: interpola o ponto na fração ao longo da geometria LineString
|
| 156 |
+
5. Se não encontrado:
|
| 157 |
+
- Gera sugestões dos números mais próximos (5 pares + 5 ímpares)
|
| 158 |
+
- Se diferença ≤ 200 e auto_200=True: corrige automaticamente e interpola
|
| 159 |
+
- Senão: registra na tabela de falhas
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
df: DataFrame com col_cdlog e col_num (pode ter _idx; criado se ausente)
|
| 163 |
+
col_cdlog: Nome da coluna com o código do logradouro (CDLOG/CTM)
|
| 164 |
+
col_num: Nome da coluna com o número predial
|
| 165 |
+
auto_200: Se True, corrige automaticamente números com diferença ≤ 200
|
| 166 |
+
|
| 167 |
+
Retorna:
|
| 168 |
+
df_resultado: DataFrame completo com colunas 'lat' e 'lon' (None para falhas)
|
| 169 |
+
df_falhas: DataFrame com falhas para correção manual
|
| 170 |
+
ajustados: Lista de dicts {idx, numero_original, numero_usado} auto-corrigidos
|
| 171 |
+
"""
|
| 172 |
+
gdf_eixos = carregar_eixos()
|
| 173 |
+
if gdf_eixos is None:
|
| 174 |
+
raise RuntimeError(
|
| 175 |
+
"Shapefile de eixos não disponível. "
|
| 176 |
+
"Verifique se geopandas e fiona estão instalados e se o arquivo "
|
| 177 |
+
f"'{_SHAPEFILE}' existe."
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
df = df.copy()
|
| 181 |
+
|
| 182 |
+
# Garante coluna _idx para rastreamento
|
| 183 |
+
if "_idx" not in df.columns:
|
| 184 |
+
df["_idx"] = range(len(df))
|
| 185 |
+
|
| 186 |
+
df[col_num] = pd.to_numeric(df[col_num], errors="coerce").fillna(0).astype(int)
|
| 187 |
+
|
| 188 |
+
lats = []
|
| 189 |
+
lons = []
|
| 190 |
+
falhas = []
|
| 191 |
+
ajustados = []
|
| 192 |
+
|
| 193 |
+
for _, row in df.iterrows():
|
| 194 |
+
idx = row["_idx"]
|
| 195 |
+
cdlog = row[col_cdlog]
|
| 196 |
+
numero = int(row[col_num])
|
| 197 |
+
|
| 198 |
+
# --- Passo 1: buscar segmentos do CDLOG ---
|
| 199 |
+
segmentos = gdf_eixos[gdf_eixos["CDLOG"] == cdlog]
|
| 200 |
+
|
| 201 |
+
if segmentos.empty:
|
| 202 |
+
lats.append(None)
|
| 203 |
+
lons.append(None)
|
| 204 |
+
falhas.append({
|
| 205 |
+
"_idx": idx,
|
| 206 |
+
"cdlog": cdlog,
|
| 207 |
+
"numero_atual": numero,
|
| 208 |
+
"motivo": "CDLOG não encontrado",
|
| 209 |
+
"sugestoes": "",
|
| 210 |
+
"numero_corrigido": "",
|
| 211 |
+
})
|
| 212 |
+
continue
|
| 213 |
+
|
| 214 |
+
# --- Passo 2: determinar lado par/ímpar ---
|
| 215 |
+
lado = "Par" if numero % 2 == 0 else "Ímpar"
|
| 216 |
+
ini_col, fim_col = (
|
| 217 |
+
("NRPARINI", "NRPARFIN") if lado == "Par" else ("NRIMPINI", "NRIMPFIN")
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
segmentos = segmentos.copy()
|
| 221 |
+
segmentos[ini_col] = pd.to_numeric(segmentos[ini_col], errors="coerce")
|
| 222 |
+
segmentos[fim_col] = pd.to_numeric(segmentos[fim_col], errors="coerce")
|
| 223 |
+
segmentos = segmentos.dropna(subset=[ini_col, fim_col])
|
| 224 |
+
|
| 225 |
+
# --- Passo 3: encontrar segmento com intervalo válido ---
|
| 226 |
+
cond = (segmentos[ini_col] <= numero) & (segmentos[fim_col] >= numero)
|
| 227 |
+
segmentos_validos = segmentos[cond]
|
| 228 |
+
|
| 229 |
+
if segmentos_validos.empty:
|
| 230 |
+
# --- Passo 5: intervalo não encontrado — gera sugestões ---
|
| 231 |
+
sugestoes_str = ""
|
| 232 |
+
numero_para_interpolar = None
|
| 233 |
+
|
| 234 |
+
if not segmentos.empty:
|
| 235 |
+
diffs = (segmentos[ini_col] - numero).abs()
|
| 236 |
+
min_index = diffs.idxmin()
|
| 237 |
+
linha_proxima = segmentos.loc[min_index]
|
| 238 |
+
|
| 239 |
+
ini = linha_proxima[ini_col]
|
| 240 |
+
fim = linha_proxima[fim_col]
|
| 241 |
+
|
| 242 |
+
if pd.notna(ini) and pd.notna(fim):
|
| 243 |
+
ini_i, fim_i = int(ini), int(fim)
|
| 244 |
+
todos = list(range(ini_i, fim_i + 1))
|
| 245 |
+
pares = sorted([n for n in todos if n % 2 == 0], key=lambda x: abs(x - numero))
|
| 246 |
+
impares = sorted([n for n in todos if n % 2 != 0], key=lambda x: abs(x - numero))
|
| 247 |
+
sugestoes = sorted(pares[:5] + impares[:5], key=lambda x: abs(x - numero))
|
| 248 |
+
sugestoes_str = ", ".join(map(str, sugestoes))
|
| 249 |
+
|
| 250 |
+
# Auto-correção ≤ 200
|
| 251 |
+
if sugestoes and auto_200:
|
| 252 |
+
melhor = sugestoes[0]
|
| 253 |
+
diferenca = abs(numero - melhor)
|
| 254 |
+
if diferenca <= 200:
|
| 255 |
+
numero_para_interpolar = melhor
|
| 256 |
+
ajustados.append({
|
| 257 |
+
"idx": idx,
|
| 258 |
+
"numero_original": numero,
|
| 259 |
+
"numero_usado": melhor,
|
| 260 |
+
})
|
| 261 |
+
|
| 262 |
+
if numero_para_interpolar is not None:
|
| 263 |
+
# Interpola com o número corrigido automaticamente
|
| 264 |
+
cond2 = (segmentos[ini_col] <= numero_para_interpolar) & \
|
| 265 |
+
(segmentos[fim_col] >= numero_para_interpolar)
|
| 266 |
+
seg2 = segmentos[cond2]
|
| 267 |
+
if seg2.empty:
|
| 268 |
+
# Usa segmento mais próximo mesmo assim
|
| 269 |
+
seg2 = segmentos.loc[[min_index]]
|
| 270 |
+
|
| 271 |
+
linha = seg2.iloc[0]
|
| 272 |
+
geom = linha.geometry
|
| 273 |
+
ini_v = linha[ini_col]
|
| 274 |
+
fim_v = linha[fim_col]
|
| 275 |
+
|
| 276 |
+
if fim_v == ini_v:
|
| 277 |
+
lats.append(None)
|
| 278 |
+
lons.append(None)
|
| 279 |
+
else:
|
| 280 |
+
frac = (numero_para_interpolar - ini_v) / (fim_v - ini_v)
|
| 281 |
+
frac = max(0.0, min(1.0, frac))
|
| 282 |
+
ponto = geom.interpolate(geom.length * frac)
|
| 283 |
+
lons.append(ponto.x)
|
| 284 |
+
lats.append(ponto.y)
|
| 285 |
+
else:
|
| 286 |
+
lats.append(None)
|
| 287 |
+
lons.append(None)
|
| 288 |
+
falhas.append({
|
| 289 |
+
"_idx": idx,
|
| 290 |
+
"cdlog": cdlog,
|
| 291 |
+
"numero_atual": numero,
|
| 292 |
+
"motivo": "Numeração fora do intervalo",
|
| 293 |
+
"sugestoes": sugestoes_str,
|
| 294 |
+
"numero_corrigido": "",
|
| 295 |
+
})
|
| 296 |
+
continue
|
| 297 |
+
|
| 298 |
+
# --- Passo 4: interpolação normal ---
|
| 299 |
+
linha = segmentos_validos.iloc[0]
|
| 300 |
+
geom = linha.geometry
|
| 301 |
+
ini = linha[ini_col]
|
| 302 |
+
fim = linha[fim_col]
|
| 303 |
+
|
| 304 |
+
if fim == ini:
|
| 305 |
+
lats.append(None)
|
| 306 |
+
lons.append(None)
|
| 307 |
+
continue
|
| 308 |
+
|
| 309 |
+
frac = (numero - ini) / (fim - ini)
|
| 310 |
+
frac = max(0.0, min(1.0, frac))
|
| 311 |
+
ponto = geom.interpolate(geom.length * frac)
|
| 312 |
+
lons.append(ponto.x)
|
| 313 |
+
lats.append(ponto.y)
|
| 314 |
+
|
| 315 |
+
df["lat"] = lats
|
| 316 |
+
df["lon"] = lons
|
| 317 |
+
|
| 318 |
+
df_falhas = pd.DataFrame(
|
| 319 |
+
falhas,
|
| 320 |
+
columns=["_idx", "cdlog", "numero_atual", "motivo", "sugestoes", "numero_corrigido"]
|
| 321 |
+
) if falhas else pd.DataFrame(
|
| 322 |
+
columns=["_idx", "cdlog", "numero_atual", "motivo", "sugestoes", "numero_corrigido"]
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
return df, df_falhas, ajustados
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def aplicar_correcoes_e_regeodificar(df_original, df_falhas, col_cdlog, col_num, auto_200=False):
|
| 329 |
+
"""Aplica correções manuais da tabela de falhas e re-executa a geocodificação.
|
| 330 |
+
|
| 331 |
+
Args:
|
| 332 |
+
df_original: DataFrame acumulado da rodada anterior (com coluna _idx e col_num já corrigido)
|
| 333 |
+
df_falhas: DataFrame editável com coluna 'numero_corrigido' preenchida pelo usuário
|
| 334 |
+
col_cdlog: Nome da coluna CDLOG
|
| 335 |
+
col_num: Nome da coluna número predial
|
| 336 |
+
auto_200: Repassado para geocodificar()
|
| 337 |
+
|
| 338 |
+
Retorna:
|
| 339 |
+
(df_resultado, df_falhas_novas, ajustados, manuais)
|
| 340 |
+
manuais: lista de _idx que receberam correção manual nesta rodada
|
| 341 |
+
"""
|
| 342 |
+
df = df_original.copy()
|
| 343 |
+
|
| 344 |
+
# Garante coluna _idx (mesma lógica de geocodificar)
|
| 345 |
+
if "_idx" not in df.columns:
|
| 346 |
+
df["_idx"] = range(len(df))
|
| 347 |
+
|
| 348 |
+
# Aplica correções e rastreia quais _idx foram corrigidos manualmente
|
| 349 |
+
manuais = []
|
| 350 |
+
for _, row in df_falhas.iterrows():
|
| 351 |
+
corrigido = str(row.get("numero_corrigido", "")).strip()
|
| 352 |
+
if corrigido and corrigido not in ("", "nan"):
|
| 353 |
+
try:
|
| 354 |
+
novo_num = int(float(corrigido))
|
| 355 |
+
idx = row["_idx"]
|
| 356 |
+
df.loc[df["_idx"] == idx, col_num] = novo_num
|
| 357 |
+
manuais.append(idx)
|
| 358 |
+
except (ValueError, TypeError):
|
| 359 |
+
pass
|
| 360 |
+
|
| 361 |
+
df_resultado, df_falhas_novas, ajustados = geocodificar(df, col_cdlog, col_num, auto_200)
|
| 362 |
+
return df_resultado, df_falhas_novas, ajustados, manuais
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
# ============================================================
|
| 366 |
+
# FORMATAÇÃO HTML DO STATUS DE GEOCODIFICAÇÃO
|
| 367 |
+
# ============================================================
|
| 368 |
+
|
| 369 |
+
def formatar_status_geocodificacao(df_resultado, df_falhas, ajustados, manuais=None):
|
| 370 |
+
"""Gera HTML de resumo do resultado da geocodificação.
|
| 371 |
+
|
| 372 |
+
Args:
|
| 373 |
+
df_resultado: DataFrame com colunas lat/lon
|
| 374 |
+
df_falhas: DataFrame de falhas restantes
|
| 375 |
+
ajustados: Lista de registros auto-corrigidos
|
| 376 |
+
manuais: Lista de _idx corrigidos manualmente nesta rodada (opcional)
|
| 377 |
+
|
| 378 |
+
Retorna:
|
| 379 |
+
String HTML com resumo colorido.
|
| 380 |
+
"""
|
| 381 |
+
total = len(df_resultado)
|
| 382 |
+
n_falhas = len(df_falhas)
|
| 383 |
+
n_ajustados = len(ajustados)
|
| 384 |
+
n_manuais = len(manuais) if manuais else 0
|
| 385 |
+
n_ok = total - n_falhas
|
| 386 |
+
|
| 387 |
+
linhas = [
|
| 388 |
+
'<div style="background:#f0f4ff;border:1px solid #c0d0f0;border-radius:8px;'
|
| 389 |
+
'padding:10px 14px;margin-top:8px">',
|
| 390 |
+
f'<strong>Resultado da geocodificação</strong> — {total} registros processados<br>',
|
| 391 |
+
f'<span style="color:#1a7a1a">✔ {n_ok} geocodificados com sucesso</span><br>',
|
| 392 |
+
]
|
| 393 |
+
|
| 394 |
+
if n_ajustados > 0:
|
| 395 |
+
linhas.append(
|
| 396 |
+
f'<span style="color:#7a5a00">✏ {n_ajustados} corrigidos automaticamente '
|
| 397 |
+
f'(diferença ≤ 200)</span><br>'
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
if n_manuais > 0:
|
| 401 |
+
linhas.append(
|
| 402 |
+
f'<span style="color:#2980b9">✎ {n_manuais} corrigidos manualmente</span><br>'
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
if n_falhas > 0:
|
| 406 |
+
linhas.append(
|
| 407 |
+
f'<span style="color:#c0392b">✘ {n_falhas} com falha — '
|
| 408 |
+
f'preencha "Nº Corrigido" na tabela abaixo e aplique as correções</span>'
|
| 409 |
+
)
|
| 410 |
+
else:
|
| 411 |
+
linhas.append('<span style="color:#1a7a1a">Nenhuma falha restante.</span>')
|
| 412 |
+
|
| 413 |
+
linhas.append("</div>")
|
| 414 |
+
return "".join(linhas)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def preparar_display_falhas(df_falhas):
|
| 418 |
+
"""Prepara exibição de falhas: tabela HTML descritiva + DataFrame mínimo para correção.
|
| 419 |
+
|
| 420 |
+
Args:
|
| 421 |
+
df_falhas: DataFrame com colunas [_idx, cdlog, numero_atual, motivo, sugestoes]
|
| 422 |
+
|
| 423 |
+
Retorna:
|
| 424 |
+
html_str: Tabela HTML legível (read-only) com as falhas
|
| 425 |
+
df_correcoes: DataFrame com colunas ["Nº Linha", "Nº Corrigido"] para edição pelo usuário
|
| 426 |
+
"""
|
| 427 |
+
if df_falhas is None or df_falhas.empty:
|
| 428 |
+
return "", pd.DataFrame(columns=["Nº Linha", "Nº Corrigido"])
|
| 429 |
+
|
| 430 |
+
linhas = [
|
| 431 |
+
'<div style="overflow-x:auto;margin-top:8px">',
|
| 432 |
+
'<table style="width:100%;border-collapse:collapse;font-size:0.9em">',
|
| 433 |
+
'<thead><tr style="background:#f0f4ff">',
|
| 434 |
+
'<th style="border:1px solid #c0d0f0;padding:6px 10px;text-align:left">Nº Linha</th>',
|
| 435 |
+
'<th style="border:1px solid #c0d0f0;padding:6px 10px;text-align:left">CDLOG</th>',
|
| 436 |
+
'<th style="border:1px solid #c0d0f0;padding:6px 10px;text-align:left">Nº Atual</th>',
|
| 437 |
+
'<th style="border:1px solid #c0d0f0;padding:6px 10px;text-align:left">Motivo</th>',
|
| 438 |
+
'<th style="border:1px solid #c0d0f0;padding:6px 10px;text-align:left">Sugestões</th>',
|
| 439 |
+
'</tr></thead><tbody>',
|
| 440 |
+
]
|
| 441 |
+
for _, row in df_falhas.iterrows():
|
| 442 |
+
linhas.append(
|
| 443 |
+
f'<tr>'
|
| 444 |
+
f'<td style="border:1px solid #dde;padding:5px 10px">{row["_idx"]}</td>'
|
| 445 |
+
f'<td style="border:1px solid #dde;padding:5px 10px">{row["cdlog"]}</td>'
|
| 446 |
+
f'<td style="border:1px solid #dde;padding:5px 10px">{row["numero_atual"]}</td>'
|
| 447 |
+
f'<td style="border:1px solid #dde;padding:5px 10px">{row["motivo"]}</td>'
|
| 448 |
+
f'<td style="border:1px solid #dde;padding:5px 10px">{row["sugestoes"]}</td>'
|
| 449 |
+
f'</tr>'
|
| 450 |
+
)
|
| 451 |
+
linhas.append('</tbody></table></div>')
|
| 452 |
+
|
| 453 |
+
df_correcoes = pd.DataFrame({
|
| 454 |
+
"Nº Linha": df_falhas["_idx"].tolist(),
|
| 455 |
+
"Nº Corrigido": [""] * len(df_falhas),
|
| 456 |
+
})
|
| 457 |
+
|
| 458 |
+
return "".join(linhas), df_correcoes
|
backend/app/core/elaboracao/modelo.py
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
modelo.py - Ajuste de modelo OLS, transformações e seleção de variáveis.
|
| 4 |
+
|
| 5 |
+
Callbacks que lidam com a lógica do modelo estatístico:
|
| 6 |
+
seleção X/Y, ajuste OLS, busca de transformações, exportação.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from datetime import datetime, timezone, timedelta
|
| 12 |
+
|
| 13 |
+
from .formatadores import (
|
| 14 |
+
criar_header_secao,
|
| 15 |
+
formatar_diagnosticos_html,
|
| 16 |
+
formatar_busca_html,
|
| 17 |
+
formatar_micronumerosidade_html,
|
| 18 |
+
formatar_aviso_multicolinearidade,
|
| 19 |
+
)
|
| 20 |
+
from .core import (
|
| 21 |
+
obter_colunas_numericas,
|
| 22 |
+
calcular_estatisticas_variaveis,
|
| 23 |
+
ajustar_modelo,
|
| 24 |
+
buscar_melhores_transformacoes,
|
| 25 |
+
testar_micronumerosidade,
|
| 26 |
+
detectar_dicotomicas,
|
| 27 |
+
detectar_codigo_alocado,
|
| 28 |
+
detectar_percentuais,
|
| 29 |
+
exportar_modelo_dai,
|
| 30 |
+
avaliar_imovel,
|
| 31 |
+
exportar_avaliacoes_excel,
|
| 32 |
+
verificar_multicolinearidade,
|
| 33 |
+
)
|
| 34 |
+
from .formatadores import formatar_avaliacao_html
|
| 35 |
+
from .charts import (
|
| 36 |
+
criar_graficos_dispersao,
|
| 37 |
+
criar_graficos_dispersao_residuos,
|
| 38 |
+
criar_painel_diagnostico,
|
| 39 |
+
criar_matriz_correlacao,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ============================================================
|
| 44 |
+
# CONSTANTES
|
| 45 |
+
# ============================================================
|
| 46 |
+
|
| 47 |
+
MAX_VARS_X = 20
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ============================================================
|
| 51 |
+
# UTILITÁRIOS DE TRANSFORMAÇÃO
|
| 52 |
+
# ============================================================
|
| 53 |
+
|
| 54 |
+
def obter_transformacoes_dos_dropdowns(colunas_x, *valores_dropdowns):
|
| 55 |
+
"""Coleta transformações dos valores dos dropdowns."""
|
| 56 |
+
transformacoes = {}
|
| 57 |
+
for i, col in enumerate(colunas_x):
|
| 58 |
+
if i < len(valores_dropdowns) and valores_dropdowns[i]:
|
| 59 |
+
transformacoes[col] = valores_dropdowns[i]
|
| 60 |
+
else:
|
| 61 |
+
transformacoes[col] = "(x)"
|
| 62 |
+
return transformacoes
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def atualizar_campos_transformacoes(df, colunas_x, dicotomicas=None, coluna_y=None):
|
| 66 |
+
"""Atualiza visibilidade e valores dos campos de transformação.
|
| 67 |
+
|
| 68 |
+
CONTRACT: Retorna 66 itens para transf_y_row(1) + transf_y_col(1) + transf_y_label(1)
|
| 69 |
+
+ transf_x_rows(3) + transf_x_columns(20) + transf_x_labels(20) + transf_x_dropdowns(20).
|
| 70 |
+
Se alterar, atualizar: app.py (outputs de checkboxes_x.change e btn_aplicar_selecao_x.click)
|
| 71 |
+
+ _atualizar_campos_transformacoes_com_flag (neste arquivo)
|
| 72 |
+
+ aplicar_selecao_callback (neste arquivo, campos_vazios).
|
| 73 |
+
"""
|
| 74 |
+
n_rows = (MAX_VARS_X + 7) // 8 # 3 rows (8 cards por linha)
|
| 75 |
+
|
| 76 |
+
# Y card updates
|
| 77 |
+
if coluna_y and df is not None and colunas_x:
|
| 78 |
+
y_label_html = f'<span class="transf-label-text transf-label-y">{coluna_y} (Y)</span>'
|
| 79 |
+
y_updates = [
|
| 80 |
+
gr.update(visible=True), # transf_y_row
|
| 81 |
+
gr.update(visible=True), # transf_y_col
|
| 82 |
+
gr.update(value=y_label_html, visible=True), # transf_y_label
|
| 83 |
+
]
|
| 84 |
+
else:
|
| 85 |
+
y_updates = [
|
| 86 |
+
gr.update(visible=False), # transf_y_row
|
| 87 |
+
gr.update(visible=False), # transf_y_col
|
| 88 |
+
gr.update(value="", visible=False), # transf_y_label
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
if df is None or not colunas_x:
|
| 92 |
+
# Esconde todos
|
| 93 |
+
row_updates = [gr.update(visible=False)] * n_rows
|
| 94 |
+
column_updates = [gr.update(visible=False)] * MAX_VARS_X
|
| 95 |
+
label_updates = [gr.update(value="", visible=False)] * MAX_VARS_X
|
| 96 |
+
dropdown_updates = [gr.update(value="(x)", interactive=True, visible=False)] * MAX_VARS_X
|
| 97 |
+
return y_updates + row_updates + column_updates + label_updates + dropdown_updates
|
| 98 |
+
|
| 99 |
+
if dicotomicas is None:
|
| 100 |
+
dicotomicas = detectar_dicotomicas(df, colunas_x)
|
| 101 |
+
|
| 102 |
+
# Updates para rows (visibilidade) - 8 por linha
|
| 103 |
+
row_updates = []
|
| 104 |
+
for i in range(n_rows):
|
| 105 |
+
idx_base = i * 8
|
| 106 |
+
visivel = idx_base < len(colunas_x)
|
| 107 |
+
row_updates.append(gr.update(visible=visivel))
|
| 108 |
+
|
| 109 |
+
# Updates para columns, labels e dropdowns
|
| 110 |
+
column_updates = []
|
| 111 |
+
label_updates = []
|
| 112 |
+
dropdown_updates = []
|
| 113 |
+
|
| 114 |
+
for i in range(MAX_VARS_X):
|
| 115 |
+
if i < len(colunas_x):
|
| 116 |
+
col = colunas_x[i]
|
| 117 |
+
eh_marcada = col in dicotomicas
|
| 118 |
+
# Só trava transformação se variável marcada E tem valores zero (dicotômica 0/1)
|
| 119 |
+
travar = False
|
| 120 |
+
if eh_marcada and col in df.columns:
|
| 121 |
+
valores_col = set(df[col].dropna().unique())
|
| 122 |
+
travar = 0 in valores_col or 0.0 in valores_col
|
| 123 |
+
column_updates.append(gr.update(visible=True))
|
| 124 |
+
label_updates.append(gr.update(value=f'<span class="transf-label-text">{col}</span>', visible=True))
|
| 125 |
+
dropdown_updates.append(gr.update(
|
| 126 |
+
value="(x)",
|
| 127 |
+
interactive=not travar,
|
| 128 |
+
visible=True
|
| 129 |
+
))
|
| 130 |
+
else:
|
| 131 |
+
column_updates.append(gr.update(visible=False))
|
| 132 |
+
label_updates.append(gr.update(value="", visible=False))
|
| 133 |
+
dropdown_updates.append(gr.update(value="(x)", interactive=True, visible=False))
|
| 134 |
+
|
| 135 |
+
return y_updates + row_updates + column_updates + label_updates + dropdown_updates
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def _atualizar_campos_transformacoes_com_flag(df, colunas_x, flag_carregamento, coluna_y=None):
|
| 139 |
+
"""Wrapper que pula reset de transformações durante carregamento programático.
|
| 140 |
+
|
| 141 |
+
CONTRACT: Retorna 70 itens (66 campos + estado_flag_carregamento + 3 checkboxes).
|
| 142 |
+
Se alterar, atualizar: app.py (outputs de checkboxes_x.change).
|
| 143 |
+
"""
|
| 144 |
+
# Flag é um contador inteiro: decrementa a cada side-effect processado
|
| 145 |
+
if flag_carregamento:
|
| 146 |
+
n_items = 3 + (MAX_VARS_X + 7) // 8 + MAX_VARS_X + MAX_VARS_X + MAX_VARS_X # 66
|
| 147 |
+
return [gr.update() for _ in range(n_items)] + [max(0, int(flag_carregamento) - 1), gr.update(), gr.update(), gr.update()]
|
| 148 |
+
|
| 149 |
+
if df is not None and colunas_x:
|
| 150 |
+
dicotomicas_01 = detectar_dicotomicas(df, colunas_x)
|
| 151 |
+
codigo_alocado = detectar_codigo_alocado(df, colunas_x)
|
| 152 |
+
percentuais = detectar_percentuais(df, colunas_x)
|
| 153 |
+
else:
|
| 154 |
+
dicotomicas_01 = []
|
| 155 |
+
codigo_alocado = []
|
| 156 |
+
percentuais = []
|
| 157 |
+
|
| 158 |
+
todas_marcadas = dicotomicas_01 + codigo_alocado + percentuais
|
| 159 |
+
campos = atualizar_campos_transformacoes(df, colunas_x, todas_marcadas, coluna_y=coluna_y)
|
| 160 |
+
choices = list(colunas_x) if colunas_x else []
|
| 161 |
+
vis = bool(colunas_x)
|
| 162 |
+
return campos + [
|
| 163 |
+
False,
|
| 164 |
+
gr.update(choices=choices, value=dicotomicas_01, visible=vis),
|
| 165 |
+
gr.update(choices=choices, value=codigo_alocado, visible=vis),
|
| 166 |
+
gr.update(choices=choices, value=percentuais, visible=vis),
|
| 167 |
+
]
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ============================================================
|
| 171 |
+
# HANDLERS DE MUDANÇA DE SELEÇÃO
|
| 172 |
+
# ============================================================
|
| 173 |
+
|
| 174 |
+
def ao_mudar_tipo_grafico(tipo, resultado_modelo):
|
| 175 |
+
"""Atualiza o gráfico de dispersão com base na seleção do dropdown."""
|
| 176 |
+
if not resultado_modelo:
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
X_transf = resultado_modelo["X_transformado"]
|
| 181 |
+
|
| 182 |
+
if "Resíduo" in tipo:
|
| 183 |
+
# Gráfico X vs Resíduos
|
| 184 |
+
tabela = resultado_modelo.get("tabela_obs_calc")
|
| 185 |
+
if tabela is not None and "Resíduo Pad." in tabela.columns:
|
| 186 |
+
residuos = tabela["Resíduo Pad."].values
|
| 187 |
+
return criar_graficos_dispersao_residuos(X_transf, residuos)
|
| 188 |
+
else:
|
| 189 |
+
return None
|
| 190 |
+
else:
|
| 191 |
+
# Gráfico X vs Y (Padrão)
|
| 192 |
+
y_transf = resultado_modelo["y_transformado"]
|
| 193 |
+
return criar_graficos_dispersao(X_transf, y_transf)
|
| 194 |
+
|
| 195 |
+
except Exception as e:
|
| 196 |
+
print(f"Erro ao atualizar gráfico: {e}")
|
| 197 |
+
return None
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def ao_mudar_y_sem_estatisticas(df, coluna_y, flag_carregamento=False, mostrar_secao_x=False):
|
| 201 |
+
"""Callback quando variável y é alterada (sem calcular estatísticas).
|
| 202 |
+
|
| 203 |
+
Apenas atualiza os checkboxes de X disponíveis.
|
| 204 |
+
As estatísticas são calculadas apenas ao clicar em 'Aplicar Seleção' na seção 4.
|
| 205 |
+
|
| 206 |
+
Args:
|
| 207 |
+
df: DataFrame com os dados
|
| 208 |
+
coluna_y: Nome da coluna Y selecionada
|
| 209 |
+
flag_carregamento: Se True, mudança veio de carregamento programático (skip reset)
|
| 210 |
+
mostrar_secao_x: Se True, mostra a seção 4 (usado pelo botão Aplicar da seção 3)
|
| 211 |
+
"""
|
| 212 |
+
# Se mudança veio de carregamento programático, não sobrescrever valores
|
| 213 |
+
# Flag é um contador inteiro: decrementa a cada side-effect processado
|
| 214 |
+
if flag_carregamento:
|
| 215 |
+
return (
|
| 216 |
+
gr.update(), # checkboxes_x — manter valor do carregamento
|
| 217 |
+
gr.update(), # header_secao_4 — manter visibilidade
|
| 218 |
+
gr.update(), # accordion_secao_4 — manter visibilidade
|
| 219 |
+
max(0, int(flag_carregamento) - 1), # estado_flag_carregamento — decrementar
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
if df is None or coluna_y is None:
|
| 223 |
+
return (
|
| 224 |
+
gr.update(choices=[]), # checkboxes_x
|
| 225 |
+
gr.update(visible=False), # header_secao_4
|
| 226 |
+
gr.update(visible=False), # accordion_secao_4
|
| 227 |
+
False, # estado_flag_carregamento
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Lista de X disponíveis (exclui y) - todas marcadas por padrão
|
| 231 |
+
colunas_x = [col for col in obter_colunas_numericas(df) if col != coluna_y]
|
| 232 |
+
|
| 233 |
+
return (
|
| 234 |
+
gr.update(choices=colunas_x, value=colunas_x), # checkboxes_x
|
| 235 |
+
gr.update(visible=mostrar_secao_x), # header_secao_4
|
| 236 |
+
gr.update(visible=mostrar_secao_x, open=mostrar_secao_x), # accordion_secao_4
|
| 237 |
+
False, # estado_flag_carregamento
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# ============================================================
|
| 242 |
+
# ESTATÍSTICAS
|
| 243 |
+
# ============================================================
|
| 244 |
+
|
| 245 |
+
def atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x):
|
| 246 |
+
"""Recalcula estatísticas automaticamente e retorna timestamp.
|
| 247 |
+
|
| 248 |
+
CONTRACT: Retorna 2 itens (estatisticas, timestamp).
|
| 249 |
+
Se alterar, atualizar: outliers.py (reiniciar_iteracao_callback, destructuring).
|
| 250 |
+
"""
|
| 251 |
+
if df_filtrado is None or coluna_y is None:
|
| 252 |
+
return None, None
|
| 253 |
+
|
| 254 |
+
# Filtra apenas colunas selecionadas + Y
|
| 255 |
+
colunas_usar = [coluna_y] + list(colunas_x) if colunas_x else [coluna_y]
|
| 256 |
+
colunas_disponiveis = [c for c in colunas_usar if c in df_filtrado.columns]
|
| 257 |
+
|
| 258 |
+
if not colunas_disponiveis:
|
| 259 |
+
return None, None
|
| 260 |
+
|
| 261 |
+
estatisticas = calcular_estatisticas_variaveis(df_filtrado, coluna_y, colunas=colunas_disponiveis)
|
| 262 |
+
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 263 |
+
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 264 |
+
|
| 265 |
+
return estatisticas.round(4), timestamp
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# ============================================================
|
| 269 |
+
# CALLBACKS PRINCIPAIS
|
| 270 |
+
# ============================================================
|
| 271 |
+
|
| 272 |
+
def ajustar_modelo_callback(df, coluna_y, colunas_x, transformacao_y, outliers_anteriores, dicotomicas, codigo_alocado, percentuais, *valores_dropdowns):
|
| 273 |
+
"""Callback para ajustar o modelo e calcular métricas de outliers.
|
| 274 |
+
|
| 275 |
+
CONTRACT: Retorna 33 itens para _outputs_ajustar (app.py:criar_aba).
|
| 276 |
+
Se alterar, atualizar: _outputs_ajustar em app.py + carregamento.py (indices em carregar_dados_de_dai).
|
| 277 |
+
"""
|
| 278 |
+
# Variáveis disponíveis para filtro (métricas + todas as colunas originais do DataFrame)
|
| 279 |
+
colunas_metricas = {"Observado", "Calculado", "Resíduo", "Resíduo Pad.", "Resíduo Stud.", "Cook"}
|
| 280 |
+
colunas_originais = [c for c in df.columns if c not in colunas_metricas] if df is not None else []
|
| 281 |
+
variaveis_filtro = ["Resíduo Pad.", "Resíduo Stud.", "Cook"] + colunas_originais
|
| 282 |
+
filtro_var_updates = [gr.update(choices=variaveis_filtro)] * 4
|
| 283 |
+
|
| 284 |
+
# Updates para seções 10-16 ocultas (7 seções × 2 = 14 itens)
|
| 285 |
+
secoes_ocultas = (
|
| 286 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_10, accordion_secao_10
|
| 287 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_11, accordion_secao_11
|
| 288 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_12, accordion_secao_12
|
| 289 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_13, accordion_secao_13
|
| 290 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_14, accordion_secao_14
|
| 291 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_15, accordion_secao_15 (Avaliação)
|
| 292 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_16, accordion_secao_16 (Exportar)
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
if df is None or coluna_y is None or not colunas_x:
|
| 296 |
+
return (
|
| 297 |
+
None, # estado_modelo
|
| 298 |
+
"", # diagnosticos_html
|
| 299 |
+
None, # tabela_coef
|
| 300 |
+
None, # tabela_obs_calc
|
| 301 |
+
None, # plot_dispersao_transf
|
| 302 |
+
gr.update(visible=False), # dropdown_tipo_grafico_dispersao
|
| 303 |
+
None, None, None, None, None, # gráficos
|
| 304 |
+
None, # tabela_metricas
|
| 305 |
+
None, # estado_metricas
|
| 306 |
+
f"Excluídos: {len(outliers_anteriores) if outliers_anteriores else 0} | A excluir: 0 | A reincluir: 0 | Total: {len(outliers_anteriores) if outliers_anteriores else 0}", # txt_resumo_outliers
|
| 307 |
+
[], # estado_avaliacoes (reset)
|
| 308 |
+
*secoes_ocultas, # seções 10-16 ocultas
|
| 309 |
+
*filtro_var_updates # atualiza dropdowns de filtro
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
# Extrai transformações dos dropdowns
|
| 313 |
+
transformacoes_x = obter_transformacoes_dos_dropdowns(colunas_x, *valores_dropdowns)
|
| 314 |
+
|
| 315 |
+
# Ajusta modelo
|
| 316 |
+
resultado = ajustar_modelo(
|
| 317 |
+
df, coluna_y, colunas_x,
|
| 318 |
+
transformacao_y, transformacoes_x
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
if resultado is None:
|
| 322 |
+
return (
|
| 323 |
+
None, # estado_modelo
|
| 324 |
+
"", # diagnosticos_html
|
| 325 |
+
None, # tabela_coef
|
| 326 |
+
None, # tabela_obs_calc
|
| 327 |
+
None, # plot_dispersao_transf
|
| 328 |
+
gr.update(visible=False), # dropdown_tipo_grafico_dispersao
|
| 329 |
+
None, None, None, None, None, # gráficos
|
| 330 |
+
None, # tabela_metricas
|
| 331 |
+
None, # estado_metricas
|
| 332 |
+
f"Outliers anteriores: {len(outliers_anteriores) if outliers_anteriores else 0} | Excluir: 0 | Reincluir: 0 | Total: {len(outliers_anteriores) if outliers_anteriores else 0}",
|
| 333 |
+
[], # estado_avaliacoes (reset)
|
| 334 |
+
*secoes_ocultas, # seções 10-16 ocultas
|
| 335 |
+
*filtro_var_updates # atualiza dropdowns de filtro
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# Formata diagnósticos
|
| 339 |
+
diagnosticos_html = formatar_diagnosticos_html(resultado["diagnosticos"])
|
| 340 |
+
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 341 |
+
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 342 |
+
|
| 343 |
+
# Gráficos de dispersão com variáveis transformadas
|
| 344 |
+
try:
|
| 345 |
+
X_transf = resultado["X_transformado"]
|
| 346 |
+
y_transf = resultado["y_transformado"]
|
| 347 |
+
fig_dispersao_transf = criar_graficos_dispersao(X_transf, y_transf)
|
| 348 |
+
except Exception as e:
|
| 349 |
+
print(f"Erro ao gerar gráficos de dispersão transformados: {e}")
|
| 350 |
+
fig_dispersao_transf = None
|
| 351 |
+
|
| 352 |
+
# Gr��ficos de diagnóstico
|
| 353 |
+
graficos = criar_painel_diagnostico(resultado)
|
| 354 |
+
|
| 355 |
+
# Correlação (com variáveis transformadas)
|
| 356 |
+
try:
|
| 357 |
+
X_transf_corr = resultado["X_transformado"].copy()
|
| 358 |
+
y_transf_corr = resultado["y_transformado"].copy()
|
| 359 |
+
|
| 360 |
+
# Cria DataFrame temporário para correlação
|
| 361 |
+
df_corr_temp = X_transf_corr
|
| 362 |
+
# Adiciona y (garantindo nome correto)
|
| 363 |
+
df_corr_temp[coluna_y] = y_transf_corr
|
| 364 |
+
|
| 365 |
+
colunas_corr = [coluna_y] + list(colunas_x)
|
| 366 |
+
fig_corr = criar_matriz_correlacao(df_corr_temp, colunas_corr)
|
| 367 |
+
except Exception as e:
|
| 368 |
+
print(f"Erro ao gerar matriz de correlação transformada: {e}")
|
| 369 |
+
fig_corr = None
|
| 370 |
+
|
| 371 |
+
# Extrai métricas de outliers da tabela_obs_calc (já contém resíduos, Cook, etc.)
|
| 372 |
+
tabela_metricas = resultado["tabela_obs_calc"].copy()
|
| 373 |
+
|
| 374 |
+
# Para o estado de filtros:
|
| 375 |
+
# 1. Usa set_index para que o índice do DataFrame seja os índices originais
|
| 376 |
+
# 2. Adiciona as variáveis X para permitir filtros por variáveis
|
| 377 |
+
tabela_metricas_estado = tabela_metricas.set_index("Índice")
|
| 378 |
+
|
| 379 |
+
# Adiciona todas as colunas originais ao estado para filtros
|
| 380 |
+
indices_usados = resultado["indices_usados"]
|
| 381 |
+
df_filtrado = df.loc[indices_usados]
|
| 382 |
+
colunas_novas = {
|
| 383 |
+
col: df_filtrado[col].values
|
| 384 |
+
for col in df.columns
|
| 385 |
+
if col not in tabela_metricas_estado.columns
|
| 386 |
+
}
|
| 387 |
+
if colunas_novas:
|
| 388 |
+
tabela_metricas_estado = pd.concat(
|
| 389 |
+
[tabela_metricas_estado, pd.DataFrame(colunas_novas, index=tabela_metricas_estado.index)],
|
| 390 |
+
axis=1
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# Resumo de outliers
|
| 394 |
+
n_anteriores = len(outliers_anteriores) if outliers_anteriores else 0
|
| 395 |
+
resumo = f"Excluídos: {n_anteriores} | A excluir: 0 | A reincluir: 0 | Total: {n_anteriores}"
|
| 396 |
+
|
| 397 |
+
# Updates para seções 10-16 visíveis com timestamp nos headers (7 seções × 2 = 14 itens)
|
| 398 |
+
secoes_visiveis = (
|
| 399 |
+
gr.update(visible=True, value=criar_header_secao(10, "Gráficos de Dispersão com Modelo Ajustado", timestamp)), # header_secao_10
|
| 400 |
+
gr.update(visible=True, open=True), # accordion_secao_10
|
| 401 |
+
gr.update(visible=True, value=criar_header_secao(11, "Diagnóstico de Modelo", timestamp)), # header_secao_11
|
| 402 |
+
gr.update(visible=True, open=True), # accordion_secao_11
|
| 403 |
+
gr.update(visible=True, value=criar_header_secao(12, "Gráficos de Diagnóstico do Modelo", timestamp)), # header_secao_12
|
| 404 |
+
gr.update(visible=True, open=True), # accordion_secao_12
|
| 405 |
+
gr.update(visible=True, value=criar_header_secao(13, "Analisar Outliers", timestamp)), # header_secao_13
|
| 406 |
+
gr.update(visible=True, open=True), # accordion_secao_13
|
| 407 |
+
gr.update(visible=True, value=criar_header_secao(14, "Exclusão ou Reinclusão de Outliers", timestamp)), # header_secao_14
|
| 408 |
+
gr.update(visible=True, open=True), # accordion_secao_14
|
| 409 |
+
gr.update(visible=True, value=criar_header_secao(15, "Avaliação de Imóvel", timestamp)), # header_secao_15
|
| 410 |
+
gr.update(visible=True, open=True), # accordion_secao_15
|
| 411 |
+
gr.update(visible=True, value=criar_header_secao(16, "Exportar Modelo", timestamp)), # header_secao_16
|
| 412 |
+
gr.update(visible=True, open=True), # accordion_secao_16
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
resultado["dicotomicas"] = list(dicotomicas) if dicotomicas else []
|
| 416 |
+
resultado["codigo_alocado"] = list(codigo_alocado) if codigo_alocado else []
|
| 417 |
+
resultado["percentuais"] = list(percentuais) if percentuais else []
|
| 418 |
+
|
| 419 |
+
return (
|
| 420 |
+
resultado,
|
| 421 |
+
diagnosticos_html,
|
| 422 |
+
resultado["tabela_coef"].round(4),
|
| 423 |
+
resultado["tabela_obs_calc"].round(4),
|
| 424 |
+
fig_dispersao_transf, # plot_dispersao_transf
|
| 425 |
+
gr.update(value="Variáveis Independentes Transformadas X Variável Dependente Transformada", visible=True), # dropdown_tipo_grafico_dispersao
|
| 426 |
+
graficos.get("obs_calc"),
|
| 427 |
+
graficos.get("residuos"),
|
| 428 |
+
graficos.get("histograma"),
|
| 429 |
+
graficos.get("cook"),
|
| 430 |
+
fig_corr,
|
| 431 |
+
tabela_metricas.round(4), # tabela_metricas (para exibição - com coluna Índice)
|
| 432 |
+
tabela_metricas_estado, # estado_metricas (com set_index - para filtros)
|
| 433 |
+
resumo, # txt_resumo_outliers
|
| 434 |
+
[], # estado_avaliacoes (reset ao re-ajustar)
|
| 435 |
+
*secoes_visiveis, # seções 10-16 visíveis
|
| 436 |
+
*filtro_var_updates # atualiza dropdowns de filtro
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def buscar_transformacoes_callback(df, coluna_y, colunas_x, dicotomicas=None, codigo_alocado=None, percentuais=None, grau_min_coef=1, grau_min_f=0):
|
| 441 |
+
"""Callback para busca automática de transformações.
|
| 442 |
+
|
| 443 |
+
CONTRACT: Retorna 8 itens (html, resultados, timestamp, btn1..btn5).
|
| 444 |
+
Se alterar, atualizar: outliers.py (reiniciar_iteracao_callback, destructuring)
|
| 445 |
+
+ app.py (outputs de btn_buscar_transf.click).
|
| 446 |
+
"""
|
| 447 |
+
btn_hidden = [gr.update(visible=False)] * 5
|
| 448 |
+
|
| 449 |
+
if df is None or coluna_y is None or not colunas_x:
|
| 450 |
+
return ("<p>Selecione as variáveis primeiro.</p>", [], "", *btn_hidden)
|
| 451 |
+
|
| 452 |
+
# Verifica se colunas têm dados válidos (não 100% NaN)
|
| 453 |
+
colunas_vazias = []
|
| 454 |
+
for col in colunas_x:
|
| 455 |
+
if col in df.columns and df[col].isna().all():
|
| 456 |
+
colunas_vazias.append(col)
|
| 457 |
+
|
| 458 |
+
if colunas_vazias:
|
| 459 |
+
msg = f"<p style='color: red;'><b>Erro:</b> As seguintes colunas estão completamente vazias (sem dados): <b>{', '.join(colunas_vazias)}</b></p>"
|
| 460 |
+
msg += "<p>Selecione apenas variáveis que contenham dados válidos.</p>"
|
| 461 |
+
return (msg, [], "", *btn_hidden)
|
| 462 |
+
|
| 463 |
+
# Verifica se Y tem dados válidos
|
| 464 |
+
if coluna_y in df.columns and df[coluna_y].isna().all():
|
| 465 |
+
msg = f"<p style='color: red;'><b>Erro:</b> A variável dependente <b>{coluna_y}</b> está completamente vazia.</p>"
|
| 466 |
+
return (msg, [], "", *btn_hidden)
|
| 467 |
+
|
| 468 |
+
# Fixa dicotômicas e percentuais em (x) — código alocado fica livre
|
| 469 |
+
transformacoes_fixas = {}
|
| 470 |
+
if dicotomicas is None:
|
| 471 |
+
dicotomicas = detectar_dicotomicas(df, colunas_x)
|
| 472 |
+
if percentuais is None:
|
| 473 |
+
percentuais = detectar_percentuais(df, colunas_x)
|
| 474 |
+
for col in (dicotomicas or []) + (percentuais or []):
|
| 475 |
+
transformacoes_fixas[col] = "(x)"
|
| 476 |
+
|
| 477 |
+
# Busca melhores transformações
|
| 478 |
+
resultados = buscar_melhores_transformacoes(
|
| 479 |
+
df, coluna_y, colunas_x,
|
| 480 |
+
transformacoes_fixas=transformacoes_fixas,
|
| 481 |
+
top_n=5,
|
| 482 |
+
grau_min_coef=int(grau_min_coef),
|
| 483 |
+
grau_min_f=int(grau_min_f)
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
if not resultados:
|
| 487 |
+
msg = (
|
| 488 |
+
"<p style='color: orange;'><b>Aviso:</b> Nenhuma combinação de transformações resultou "
|
| 489 |
+
"em todos os coeficientes com ao menos Grau I de significância (p ≤ 30%).</p>"
|
| 490 |
+
"<p>Considere revisar as variáveis selecionadas ou verificar a qualidade dos dados.</p>"
|
| 491 |
+
)
|
| 492 |
+
return (msg, [], "", *btn_hidden)
|
| 493 |
+
|
| 494 |
+
html = formatar_busca_html(resultados)
|
| 495 |
+
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 496 |
+
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 497 |
+
|
| 498 |
+
# Atualiza visibilidade dos botões "Adotar"
|
| 499 |
+
btn_updates = [gr.update(visible=(i < len(resultados))) for i in range(5)]
|
| 500 |
+
|
| 501 |
+
return (html, resultados, timestamp, *btn_updates)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def aplicar_selecao_callback(df, coluna_y, colunas_x, outliers_anteriores, dicotomicas=None, codigo_alocado=None, percentuais=None):
|
| 505 |
+
"""Aplica seleção de variáveis, atualiza estatísticas e busca transformações automaticamente.
|
| 506 |
+
|
| 507 |
+
NÃO calcula métricas de outliers aqui - isso é feito ao ajustar o modelo.
|
| 508 |
+
Filtra dados excluindo outliers de iterações anteriores.
|
| 509 |
+
Gera gráficos de dispersão e teste de micronumerosidade.
|
| 510 |
+
Busca transformações com fallback automático (começa em Grau III/III e reduz se necessário).
|
| 511 |
+
|
| 512 |
+
CONTRACT: Retorna 90 itens para btn_aplicar_selecao_x.click (app.py:criar_aba).
|
| 513 |
+
Se alterar, atualizar: app.py (outputs) + carregamento.py (indices em carregar_dados_de_dai).
|
| 514 |
+
"""
|
| 515 |
+
n_rows = (MAX_VARS_X + 7) // 8 # 3 rows (8 por linha)
|
| 516 |
+
|
| 517 |
+
# Valores padrão quando não há dados
|
| 518 |
+
campos_vazios = (
|
| 519 |
+
[gr.update(visible=False)] * 3 + # 3: transf_y_row, transf_y_col, transf_y_label
|
| 520 |
+
[gr.update(visible=False)] * n_rows + # 3: transf_x_rows
|
| 521 |
+
[gr.update(visible=False)] * MAX_VARS_X + # 20: transf_x_columns
|
| 522 |
+
[gr.update(value="", visible=False)] * MAX_VARS_X + # 20: transf_x_labels
|
| 523 |
+
[gr.update(value="(x)", interactive=True, visible=False)] * MAX_VARS_X # 20: transf_x_dropdowns
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
# Botões de adotar escondidos por padrão
|
| 527 |
+
btn_hidden = [gr.update(visible=False)] * 5
|
| 528 |
+
|
| 529 |
+
# Valores padrão para dispersão e micronumerosidade
|
| 530 |
+
dispersao_vazio = None
|
| 531 |
+
micro_vazio = "<p>Clique em 'Aplicar Seleção' para verificar micronumerosidade.</p>"
|
| 532 |
+
|
| 533 |
+
# Updates para seções 5-9 ocultas
|
| 534 |
+
secoes_ocultas = (
|
| 535 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_5, accordion_secao_5
|
| 536 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_6, accordion_secao_6
|
| 537 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_7, accordion_secao_7
|
| 538 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_8, accordion_secao_8
|
| 539 |
+
gr.update(visible=False), gr.update(visible=False), # header_secao_9, accordion_secao_9
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
if df is None or coluna_y is None:
|
| 543 |
+
return (
|
| 544 |
+
None, # estado_df_filtrado
|
| 545 |
+
gr.update(value=None), # tabela_estatisticas
|
| 546 |
+
micro_vazio, # html_micronumerosidade
|
| 547 |
+
dispersao_vazio, # plot_dispersao
|
| 548 |
+
gr.update(), # slider_grau_coef (no-op)
|
| 549 |
+
gr.update(), # slider_grau_f (no-op)
|
| 550 |
+
"", # busca_html
|
| 551 |
+
[], # estado_resultados_busca
|
| 552 |
+
*btn_hidden, # botões adotar
|
| 553 |
+
*secoes_ocultas, # seções 5-9 ocultas
|
| 554 |
+
*campos_vazios, # campos de transformação
|
| 555 |
+
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
if not colunas_x:
|
| 559 |
+
return (
|
| 560 |
+
df,
|
| 561 |
+
gr.update(value=None), # tabela_estatisticas
|
| 562 |
+
micro_vazio, # html_micronumerosidade
|
| 563 |
+
dispersao_vazio, # plot_dispersao
|
| 564 |
+
gr.update(), # slider_grau_coef (no-op)
|
| 565 |
+
gr.update(), # slider_grau_f (no-op)
|
| 566 |
+
"", # busca_html
|
| 567 |
+
[], # estado_resultados_busca
|
| 568 |
+
*btn_hidden, # botões adotar
|
| 569 |
+
*secoes_ocultas, # seções 5-9 ocultas
|
| 570 |
+
*campos_vazios, # campos de transformação
|
| 571 |
+
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
# Filtra dados excluindo outliers de iterações anteriores
|
| 575 |
+
df_filtrado = df.copy()
|
| 576 |
+
if outliers_anteriores:
|
| 577 |
+
df_filtrado = df_filtrado.drop(index=outliers_anteriores, errors='ignore')
|
| 578 |
+
|
| 579 |
+
# Calcula estatísticas com dados filtrados
|
| 580 |
+
estatisticas, _ = atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x)
|
| 581 |
+
|
| 582 |
+
# Timestamp para as novas seções
|
| 583 |
+
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 584 |
+
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 585 |
+
|
| 586 |
+
# Teste de micronumerosidade
|
| 587 |
+
try:
|
| 588 |
+
resultado_micro = testar_micronumerosidade(df_filtrado, list(colunas_x),
|
| 589 |
+
dicotomicas=dicotomicas, codigo_alocado=codigo_alocado)
|
| 590 |
+
html_micro = formatar_micronumerosidade_html(resultado_micro)
|
| 591 |
+
except Exception as e:
|
| 592 |
+
print(f"Erro ao testar micronumerosidade: {e}")
|
| 593 |
+
html_micro = f"<p style='color: red;'>Erro ao calcular micronumerosidade: {e}</p>"
|
| 594 |
+
|
| 595 |
+
# Gera gráficos de dispersão
|
| 596 |
+
try:
|
| 597 |
+
X = df_filtrado[list(colunas_x)]
|
| 598 |
+
y = df_filtrado[coluna_y]
|
| 599 |
+
fig_dispersao = criar_graficos_dispersao(X, y)
|
| 600 |
+
except Exception as e:
|
| 601 |
+
print(f"Erro ao gerar gráficos de dispersão: {e}")
|
| 602 |
+
fig_dispersao = None
|
| 603 |
+
|
| 604 |
+
# Atualiza campos de transformação (junta todas as marcadas para lock)
|
| 605 |
+
todas_marcadas = (dicotomicas or []) + (codigo_alocado or []) + (percentuais or [])
|
| 606 |
+
campos_updates = atualizar_campos_transformacoes(df, colunas_x, todas_marcadas, coluna_y=coluna_y)
|
| 607 |
+
|
| 608 |
+
# Busca automática de transformações: critério mínimo (Grau I nos coeficientes, sem filtro F)
|
| 609 |
+
busca_html_result, resultados_busca, _, *btn_updates = buscar_transformacoes_callback(
|
| 610 |
+
df_filtrado, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais,
|
| 611 |
+
grau_min_coef=1, grau_min_f=0
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
# Radio buttons refletem o grau mínimo REAL presente nos resultados
|
| 615 |
+
if resultados_busca:
|
| 616 |
+
grau_coef_usado = min(
|
| 617 |
+
min(r.get("graus_coef", {}).values(), default=0)
|
| 618 |
+
for r in resultados_busca
|
| 619 |
+
)
|
| 620 |
+
grau_f_usado = min(r.get("grau_f", 0) for r in resultados_busca)
|
| 621 |
+
else:
|
| 622 |
+
grau_coef_usado = 1
|
| 623 |
+
grau_f_usado = 0
|
| 624 |
+
|
| 625 |
+
# Updates para seções 5-9 visíveis com timestamp nos headers
|
| 626 |
+
secoes_visiveis = (
|
| 627 |
+
gr.update(visible=True, value=criar_header_secao(5, "Estatísticas das Variáveis Selecionadas", timestamp)), # header_secao_5
|
| 628 |
+
gr.update(visible=True, open=True), # accordion_secao_5
|
| 629 |
+
gr.update(visible=True, value=criar_header_secao(6, "Teste de Micronumerosidade (NBR 14.653-2)", timestamp)), # header_secao_6
|
| 630 |
+
gr.update(visible=True, open=True), # accordion_secao_6
|
| 631 |
+
gr.update(visible=True, value=criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes", timestamp)), # header_secao_7
|
| 632 |
+
gr.update(visible=True, open=True), # accordion_secao_7
|
| 633 |
+
gr.update(visible=True, value=criar_header_secao(8, "Transformações Sugeridas", timestamp)), # header_secao_8
|
| 634 |
+
gr.update(visible=True, open=True), # accordion_secao_8
|
| 635 |
+
gr.update(visible=True, value=criar_header_secao(9, "Aplicação das Transformações", timestamp)), # header_secao_9
|
| 636 |
+
gr.update(visible=True, open=True), # accordion_secao_9
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
# Verifica multicolinearidade (dados brutos, antes das transformações da seção 9)
|
| 640 |
+
resultado_multi = verificar_multicolinearidade(df_filtrado, list(colunas_x))
|
| 641 |
+
html_multi, visivel_multi = formatar_aviso_multicolinearidade(resultado_multi)
|
| 642 |
+
|
| 643 |
+
return (
|
| 644 |
+
df_filtrado, # estado_df_filtrado
|
| 645 |
+
gr.update(value=estatisticas), # tabela_estatisticas
|
| 646 |
+
html_micro, # html_micronumerosidade
|
| 647 |
+
fig_dispersao, # plot_dispersao
|
| 648 |
+
gr.update(value=grau_coef_usado), # slider_grau_coef
|
| 649 |
+
gr.update(value=grau_f_usado), # slider_grau_f
|
| 650 |
+
busca_html_result, # busca_html
|
| 651 |
+
resultados_busca, # estado_resultados_busca
|
| 652 |
+
*btn_updates, # botões adotar
|
| 653 |
+
*secoes_visiveis, # seções 5-9 visíveis
|
| 654 |
+
*campos_updates, # campos de transformação
|
| 655 |
+
gr.update(value=html_multi, visible=visivel_multi), # html_aviso_multicolinearidade (item 90)
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def adotar_sugestao(indice, resultados, colunas_x):
|
| 660 |
+
"""Preenche os dropdowns com a sugestão selecionada.
|
| 661 |
+
|
| 662 |
+
CONTRACT: Retorna 21 itens (1 transf_y + 20 transf_x_dropdowns).
|
| 663 |
+
Se alterar, atualizar: app.py (outputs de btn_adotar_N.click).
|
| 664 |
+
"""
|
| 665 |
+
if not resultados or indice >= len(resultados):
|
| 666 |
+
return [gr.update()] * (1 + MAX_VARS_X)
|
| 667 |
+
|
| 668 |
+
sugestao = resultados[indice]
|
| 669 |
+
|
| 670 |
+
# Update para transformação de Y
|
| 671 |
+
updates = [gr.update(value=sugestao["transformacao_y"])]
|
| 672 |
+
|
| 673 |
+
# Updates para transformações de X
|
| 674 |
+
transf_x = sugestao["transformacoes_x"]
|
| 675 |
+
for i in range(MAX_VARS_X):
|
| 676 |
+
if i < len(colunas_x):
|
| 677 |
+
col = colunas_x[i]
|
| 678 |
+
updates.append(gr.update(value=transf_x.get(col, "(x)")))
|
| 679 |
+
else:
|
| 680 |
+
updates.append(gr.update())
|
| 681 |
+
|
| 682 |
+
return updates
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
# ============================================================
|
| 686 |
+
# CALLBACKS DE AVALIAÇÃO (Seção 15)
|
| 687 |
+
# ============================================================
|
| 688 |
+
|
| 689 |
+
N_AVAL_ROWS = MAX_VARS_X // 4 # 5 rows (4 inputs por linha)
|
| 690 |
+
|
| 691 |
+
|
| 692 |
+
def popular_campos_avaliacao_callback(estado_modelo, tabela_estatisticas):
|
| 693 |
+
"""Popula inputs da seção 15 com labels dinâmicos após ajuste.
|
| 694 |
+
|
| 695 |
+
Chamado via .then() após ajustar_modelo_callback ou carregar .dai.
|
| 696 |
+
|
| 697 |
+
CONTRACT: Retorna 27 itens (5 aval_rows + 20 aval_inputs + resultado_html + dropdown_base).
|
| 698 |
+
Se alterar, atualizar: app.py (_outputs_popular_avaliacao).
|
| 699 |
+
"""
|
| 700 |
+
rows_hidden = [gr.update(visible=False)] * N_AVAL_ROWS
|
| 701 |
+
inputs_hidden = [gr.update(visible=False, value=None, label="")] * MAX_VARS_X
|
| 702 |
+
dropdown_reset = gr.update(choices=[], value=None)
|
| 703 |
+
|
| 704 |
+
if estado_modelo is None:
|
| 705 |
+
return (*rows_hidden, *inputs_hidden, "", dropdown_reset)
|
| 706 |
+
|
| 707 |
+
try:
|
| 708 |
+
colunas_x = estado_modelo["colunas_x"]
|
| 709 |
+
n_vars = len(colunas_x)
|
| 710 |
+
|
| 711 |
+
# Extrair min/max das estatísticas
|
| 712 |
+
import pandas as pd
|
| 713 |
+
if tabela_estatisticas is not None and isinstance(tabela_estatisticas, pd.DataFrame):
|
| 714 |
+
if "Variável" in tabela_estatisticas.columns:
|
| 715 |
+
est_idx = tabela_estatisticas.set_index("Variável")
|
| 716 |
+
else:
|
| 717 |
+
est_idx = tabela_estatisticas
|
| 718 |
+
else:
|
| 719 |
+
est_idx = pd.DataFrame()
|
| 720 |
+
|
| 721 |
+
# Rows visíveis
|
| 722 |
+
import math
|
| 723 |
+
n_rows_vis = math.ceil(n_vars / 4)
|
| 724 |
+
rows_updates = [gr.update(visible=(r < n_rows_vis)) for r in range(N_AVAL_ROWS)]
|
| 725 |
+
|
| 726 |
+
# Inputs com labels dinâmicos
|
| 727 |
+
dicotomicas = estado_modelo.get("dicotomicas", [])
|
| 728 |
+
codigo_alocado = estado_modelo.get("codigo_alocado", [])
|
| 729 |
+
percentuais = estado_modelo.get("percentuais", [])
|
| 730 |
+
inputs_updates = []
|
| 731 |
+
for i in range(MAX_VARS_X):
|
| 732 |
+
if i < n_vars:
|
| 733 |
+
col = colunas_x[i]
|
| 734 |
+
if col in dicotomicas:
|
| 735 |
+
placeholder = "0 ou 1"
|
| 736 |
+
elif col in codigo_alocado and col in est_idx.index:
|
| 737 |
+
min_val = est_idx.loc[col, "Mínimo"]
|
| 738 |
+
max_val = est_idx.loc[col, "Máximo"]
|
| 739 |
+
placeholder = f"cód. {int(min_val)} a {int(max_val)}"
|
| 740 |
+
elif col in percentuais:
|
| 741 |
+
placeholder = "0 a 1"
|
| 742 |
+
elif col in est_idx.index:
|
| 743 |
+
min_val = est_idx.loc[col, "Mínimo"]
|
| 744 |
+
max_val = est_idx.loc[col, "Máximo"]
|
| 745 |
+
placeholder = f"{min_val} — {max_val}"
|
| 746 |
+
else:
|
| 747 |
+
placeholder = ""
|
| 748 |
+
inputs_updates.append(gr.update(visible=True, value=None, label=col, placeholder=placeholder, interactive=True))
|
| 749 |
+
else:
|
| 750 |
+
inputs_updates.append(gr.update(visible=False, value=None, label="", placeholder=""))
|
| 751 |
+
|
| 752 |
+
return (*rows_updates, *inputs_updates, "", dropdown_reset)
|
| 753 |
+
|
| 754 |
+
except Exception as e:
|
| 755 |
+
print(f"Erro ao popular campos de avaliação: {e}")
|
| 756 |
+
return (*rows_hidden, *inputs_hidden, "", dropdown_reset)
|
| 757 |
+
|
| 758 |
+
|
| 759 |
+
def avaliar_imovel_callback(estado_modelo, tabela_estatisticas, estado_avaliacoes, indice_base_str, *aval_inputs):
|
| 760 |
+
"""Avalia um imóvel usando o modelo ajustado (seção 15).
|
| 761 |
+
|
| 762 |
+
CONTRACT: Retorna 3 itens (resultado_html, estado_avaliacoes, dropdown_update).
|
| 763 |
+
Se alterar, atualizar: app.py (outputs de btn_calcular_avaliacao.click).
|
| 764 |
+
"""
|
| 765 |
+
_err = lambda msg: (msg, estado_avaliacoes or [], gr.update())
|
| 766 |
+
if estado_modelo is None:
|
| 767 |
+
return _err("Ajuste um modelo primeiro.")
|
| 768 |
+
|
| 769 |
+
import pandas as pd
|
| 770 |
+
colunas_x = estado_modelo["colunas_x"]
|
| 771 |
+
n_vars = len(colunas_x)
|
| 772 |
+
|
| 773 |
+
# Extrair valores dos inputs
|
| 774 |
+
valores_x = {}
|
| 775 |
+
for i, col in enumerate(colunas_x):
|
| 776 |
+
if i >= len(aval_inputs) or aval_inputs[i] is None:
|
| 777 |
+
return _err("Preencha todos os campos antes de calcular.")
|
| 778 |
+
valores_x[col] = float(aval_inputs[i])
|
| 779 |
+
|
| 780 |
+
# Extrair transformações do resultado do modelo
|
| 781 |
+
transformacoes_x = estado_modelo.get("transformacoes_x", {})
|
| 782 |
+
transformacao_y = estado_modelo.get("transformacao_y", "(x)")
|
| 783 |
+
|
| 784 |
+
# Obter estatísticas
|
| 785 |
+
if tabela_estatisticas is not None and isinstance(tabela_estatisticas, pd.DataFrame):
|
| 786 |
+
estatisticas_df = tabela_estatisticas
|
| 787 |
+
else:
|
| 788 |
+
return _err("Estatísticas não disponíveis.")
|
| 789 |
+
|
| 790 |
+
# Validar variáveis dicotômicas, código alocado e percentuais ANTES de avaliar
|
| 791 |
+
dicotomicas = estado_modelo.get("dicotomicas", [])
|
| 792 |
+
codigo_alocado = estado_modelo.get("codigo_alocado", [])
|
| 793 |
+
percentuais = estado_modelo.get("percentuais", [])
|
| 794 |
+
|
| 795 |
+
if "Variável" in estatisticas_df.columns:
|
| 796 |
+
est_idx = estatisticas_df.set_index("Variável")
|
| 797 |
+
else:
|
| 798 |
+
est_idx = estatisticas_df
|
| 799 |
+
|
| 800 |
+
for col in colunas_x:
|
| 801 |
+
val = valores_x[col]
|
| 802 |
+
if col in dicotomicas:
|
| 803 |
+
if val not in (0, 0.0, 1, 1.0):
|
| 804 |
+
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é dicotômica e aceita apenas valores 0 ou 1. "
|
| 805 |
+
f"Valor informado: {val}</p>")
|
| 806 |
+
elif col in codigo_alocado and col in est_idx.index:
|
| 807 |
+
min_val = float(est_idx.loc[col, "Mínimo"])
|
| 808 |
+
max_val = float(est_idx.loc[col, "Máximo"])
|
| 809 |
+
eh_inteiro = (float(val) == int(float(val)))
|
| 810 |
+
if not eh_inteiro or val < min_val or val > max_val:
|
| 811 |
+
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é de código alocado/ajustado e aceita apenas "
|
| 812 |
+
f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
|
| 813 |
+
elif col in percentuais:
|
| 814 |
+
if val < 0 or val > 1:
|
| 815 |
+
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é percentual e aceita apenas "
|
| 816 |
+
f"valores entre 0 e 1. Valor informado: {val}</p>")
|
| 817 |
+
|
| 818 |
+
resultado = avaliar_imovel(
|
| 819 |
+
modelo_sm=estado_modelo["modelo_sm"],
|
| 820 |
+
valores_x=valores_x,
|
| 821 |
+
colunas_x=colunas_x,
|
| 822 |
+
transformacoes_x=transformacoes_x,
|
| 823 |
+
transformacao_y=transformacao_y,
|
| 824 |
+
estatisticas_df=estatisticas_df,
|
| 825 |
+
dicotomicas=dicotomicas,
|
| 826 |
+
codigo_alocado=codigo_alocado,
|
| 827 |
+
percentuais=percentuais,
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
if resultado is None:
|
| 831 |
+
return _err("Erro ao calcular avaliação.")
|
| 832 |
+
|
| 833 |
+
# Acumular resultado
|
| 834 |
+
nova_lista = list(estado_avaliacoes or []) + [resultado]
|
| 835 |
+
indice_base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 836 |
+
html = formatar_avaliacao_html(nova_lista, indice_base=indice_base, elem_id_excluir="excluir-aval-elab")
|
| 837 |
+
|
| 838 |
+
# Atualizar choices do dropdown de base
|
| 839 |
+
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 840 |
+
# Se é a primeira avaliação, setar base como "1"
|
| 841 |
+
base_val = indice_base_str if indice_base_str else "1"
|
| 842 |
+
|
| 843 |
+
return html, nova_lista, gr.update(choices=choices, value=base_val)
|
| 844 |
+
|
| 845 |
+
|
| 846 |
+
def limpar_avaliacoes_callback():
|
| 847 |
+
"""Limpa o histórico de avaliações da seção 15.
|
| 848 |
+
|
| 849 |
+
CONTRACT: Retorna 3 itens (resultado_html, estado_avaliacoes, dropdown_update).
|
| 850 |
+
Se alterar, atualizar: app.py (outputs de btn_limpar_avaliacoes.click).
|
| 851 |
+
"""
|
| 852 |
+
return "", [], gr.update(choices=[], value=None)
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
def excluir_avaliacao_callback(indice_str, estado_avaliacoes, indice_base_str):
|
| 856 |
+
"""Exclui uma avaliação da lista (seção 15).
|
| 857 |
+
|
| 858 |
+
CONTRACT: Retorna 4 itens (resultado_html, estado_avaliacoes, dropdown_update, trigger_reset).
|
| 859 |
+
"""
|
| 860 |
+
if not indice_str or not indice_str.strip() or not estado_avaliacoes:
|
| 861 |
+
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 862 |
+
|
| 863 |
+
try:
|
| 864 |
+
idx = int(indice_str.strip()) - 1
|
| 865 |
+
except ValueError:
|
| 866 |
+
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 867 |
+
|
| 868 |
+
if idx < 0 or idx >= len(estado_avaliacoes):
|
| 869 |
+
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 870 |
+
|
| 871 |
+
nova_lista = [a for i, a in enumerate(estado_avaliacoes) if i != idx]
|
| 872 |
+
|
| 873 |
+
if not nova_lista:
|
| 874 |
+
return "", [], gr.update(choices=[], value=None), ""
|
| 875 |
+
|
| 876 |
+
# Ajustar índice base
|
| 877 |
+
base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 878 |
+
if base >= len(nova_lista):
|
| 879 |
+
base = len(nova_lista) - 1
|
| 880 |
+
if base < 0:
|
| 881 |
+
base = 0
|
| 882 |
+
|
| 883 |
+
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 884 |
+
html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-elab")
|
| 885 |
+
return html, nova_lista, gr.update(choices=choices, value=str(base + 1)), ""
|
| 886 |
+
|
| 887 |
+
|
| 888 |
+
def atualizar_base_avaliacao_callback(estado_avaliacoes, indice_base_str):
|
| 889 |
+
"""Re-renderiza HTML quando o dropdown de base muda (seção 15).
|
| 890 |
+
|
| 891 |
+
CONTRACT: Retorna 1 item (resultado_html).
|
| 892 |
+
"""
|
| 893 |
+
if not estado_avaliacoes:
|
| 894 |
+
return ""
|
| 895 |
+
indice = int(indice_base_str) - 1 if indice_base_str else 0
|
| 896 |
+
return formatar_avaliacao_html(estado_avaliacoes, indice_base=indice, elem_id_excluir="excluir-aval-elab")
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
def exportar_avaliacoes_excel_callback(estado_avaliacoes):
|
| 900 |
+
"""Exporta avaliações para Excel (seção 15).
|
| 901 |
+
|
| 902 |
+
CONTRACT: Retorna 1 item (download_file_update).
|
| 903 |
+
"""
|
| 904 |
+
if not estado_avaliacoes:
|
| 905 |
+
return gr.update(value=None, visible=False)
|
| 906 |
+
caminho = exportar_avaliacoes_excel(estado_avaliacoes)
|
| 907 |
+
if caminho:
|
| 908 |
+
return gr.update(value=caminho, visible=True)
|
| 909 |
+
return gr.update(value=None, visible=False)
|
| 910 |
+
|
| 911 |
+
|
| 912 |
+
def exportar_modelo_callback(resultado_modelo, df, df_completo, estatisticas,
|
| 913 |
+
nome_arquivo, elaborador=None, outliers_excluidos=None):
|
| 914 |
+
"""Callback para exportar modelo com download."""
|
| 915 |
+
if resultado_modelo is None:
|
| 916 |
+
return "Nenhum modelo para exportar. Ajuste o modelo primeiro.", gr.update(value=None, visible=False)
|
| 917 |
+
|
| 918 |
+
if not nome_arquivo or not nome_arquivo.strip():
|
| 919 |
+
return "Informe o nome do arquivo.", gr.update(value=None, visible=False)
|
| 920 |
+
|
| 921 |
+
caminho, msg = exportar_modelo_dai(
|
| 922 |
+
resultado_modelo, df, df_completo, estatisticas, nome_arquivo.strip(),
|
| 923 |
+
elaborador=elaborador, outliers_excluidos=outliers_excluidos or []
|
| 924 |
+
)
|
| 925 |
+
|
| 926 |
+
if caminho:
|
| 927 |
+
return msg, gr.update(value=caminho, visible=True)
|
| 928 |
+
else:
|
| 929 |
+
return msg, gr.update(value=None, visible=False)
|
| 930 |
+
|
| 931 |
+
|
| 932 |
+
# ============================================================
|
| 933 |
+
# CALLBACKS DE DICOTÔMICAS
|
| 934 |
+
# ============================================================
|
| 935 |
+
|
| 936 |
+
def atualizar_interativo_dicotomicas(colunas_x, dicotomicas, codigo_alocado, percentuais, df):
|
| 937 |
+
"""Atualiza interactive dos dropdowns de transformação conforme os 3 tipos.
|
| 938 |
+
|
| 939 |
+
Dicotômicas e percentuais: transformação travada em (x).
|
| 940 |
+
Código alocado: transformação livre.
|
| 941 |
+
|
| 942 |
+
Handler para checkboxes_dicotomicas.change(), checkboxes_codigo_alocado.change(),
|
| 943 |
+
checkboxes_percentuais.change().
|
| 944 |
+
|
| 945 |
+
CONTRACT: Retorna 20 itens (transf_x_dropdowns).
|
| 946 |
+
Se alterar, atualizar: app.py (outputs dos .change dos 3 checkboxes).
|
| 947 |
+
"""
|
| 948 |
+
updates = []
|
| 949 |
+
for i in range(MAX_VARS_X):
|
| 950 |
+
if i < len(colunas_x):
|
| 951 |
+
col = colunas_x[i]
|
| 952 |
+
if col in (dicotomicas or []) or col in (percentuais or []):
|
| 953 |
+
updates.append(gr.update(interactive=False, value="(x)"))
|
| 954 |
+
else:
|
| 955 |
+
updates.append(gr.update(interactive=True))
|
| 956 |
+
else:
|
| 957 |
+
updates.append(gr.update())
|
| 958 |
+
return updates
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
def popular_dicotomicas_callback(estado_modelo, colunas_x, estado_df):
|
| 962 |
+
"""Popula os 3 checkboxes: dicotômicas, código alocado e percentuais.
|
| 963 |
+
|
| 964 |
+
Para .dai: usa listas salvas no modelo.
|
| 965 |
+
Para CSV/Excel: auto-detecta os 3 tipos.
|
| 966 |
+
|
| 967 |
+
CONTRACT: Retorna 3 itens (checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais).
|
| 968 |
+
Se alterar, atualizar: app.py (outputs do .then de popular_dicotomicas).
|
| 969 |
+
"""
|
| 970 |
+
empty = gr.update(choices=[], value=[], visible=False)
|
| 971 |
+
if not colunas_x:
|
| 972 |
+
return empty, empty, empty
|
| 973 |
+
|
| 974 |
+
choices = list(colunas_x)
|
| 975 |
+
|
| 976 |
+
if estado_modelo is not None:
|
| 977 |
+
dic = estado_modelo.get("dicotomicas", [])
|
| 978 |
+
cod = estado_modelo.get("codigo_alocado", [])
|
| 979 |
+
perc = estado_modelo.get("percentuais", [])
|
| 980 |
+
elif estado_df is not None:
|
| 981 |
+
dic = detectar_dicotomicas(estado_df, colunas_x)
|
| 982 |
+
cod = detectar_codigo_alocado(estado_df, colunas_x)
|
| 983 |
+
perc = detectar_percentuais(estado_df, colunas_x)
|
| 984 |
+
else:
|
| 985 |
+
return empty, empty, empty
|
| 986 |
+
|
| 987 |
+
return (
|
| 988 |
+
gr.update(choices=choices, value=dic, visible=True),
|
| 989 |
+
gr.update(choices=choices, value=cod, visible=True),
|
| 990 |
+
gr.update(choices=choices, value=perc, visible=True),
|
| 991 |
+
)
|
backend/app/core/elaboracao/outliers.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
outliers.py - Filtros dinâmicos, exclusão/reinclusão e iteração de outliers.
|
| 4 |
+
|
| 5 |
+
Callbacks que lidam com filtros de outliers, exclusão,
|
| 6 |
+
reinclusão e reinício de iteração do modelo.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
|
| 11 |
+
from .formatadores import (
|
| 12 |
+
arredondar_df,
|
| 13 |
+
criar_header_secao,
|
| 14 |
+
formatar_micronumerosidade_html,
|
| 15 |
+
formatar_outliers_anteriores_html,
|
| 16 |
+
)
|
| 17 |
+
from .modelo import buscar_transformacoes_callback, atualizar_estatisticas_auto
|
| 18 |
+
from .core import testar_micronumerosidade
|
| 19 |
+
from .charts import criar_graficos_dispersao, criar_mapa
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ============================================================
|
| 23 |
+
# FILTROS
|
| 24 |
+
# ============================================================
|
| 25 |
+
|
| 26 |
+
def aplicar_filtros_callback(metricas_estado, n_filtros, *args):
|
| 27 |
+
"""Aplica múltiplos filtros de outliers.
|
| 28 |
+
|
| 29 |
+
n_filtros: número de filtros visíveis (apenas esses serão aplicados)
|
| 30 |
+
args: var1, var2, var3, var4, op1, op2, op3, op4, val1, val2, val3, val4
|
| 31 |
+
Retorna lista de índices que satisfazem QUALQUER filtro (OR lógico).
|
| 32 |
+
"""
|
| 33 |
+
if metricas_estado is None:
|
| 34 |
+
return ""
|
| 35 |
+
|
| 36 |
+
# Extrai filtros dos argumentos (apenas os filtros visíveis)
|
| 37 |
+
# Ordem: [4 vars] + [4 ops] + [4 vals]
|
| 38 |
+
filtros = []
|
| 39 |
+
for i in range(n_filtros): # Só processa os filtros visíveis
|
| 40 |
+
var = args[i] # vars estão nos índices 0-3
|
| 41 |
+
op = args[i + 4] # ops estão nos índices 4-7
|
| 42 |
+
val = args[i + 8] # vals estão nos índices 8-11
|
| 43 |
+
if var is not None and op is not None and val is not None:
|
| 44 |
+
# Converte valor para float (pode vir como string)
|
| 45 |
+
try:
|
| 46 |
+
val_float = float(val)
|
| 47 |
+
except (ValueError, TypeError):
|
| 48 |
+
continue
|
| 49 |
+
filtros.append({"variavel": var, "operador": op, "valor": val_float})
|
| 50 |
+
|
| 51 |
+
if not filtros:
|
| 52 |
+
return ""
|
| 53 |
+
|
| 54 |
+
# Aplica filtros com lógica OR
|
| 55 |
+
indices_outliers = set()
|
| 56 |
+
|
| 57 |
+
for filtro in filtros:
|
| 58 |
+
var = filtro["variavel"]
|
| 59 |
+
op = filtro["operador"]
|
| 60 |
+
val = filtro["valor"]
|
| 61 |
+
|
| 62 |
+
if var not in metricas_estado.columns:
|
| 63 |
+
continue
|
| 64 |
+
|
| 65 |
+
# Aplica operador
|
| 66 |
+
if op == "<=":
|
| 67 |
+
mask = metricas_estado[var] <= val
|
| 68 |
+
elif op == ">=":
|
| 69 |
+
mask = metricas_estado[var] >= val
|
| 70 |
+
elif op == "<":
|
| 71 |
+
mask = metricas_estado[var] < val
|
| 72 |
+
elif op == ">":
|
| 73 |
+
mask = metricas_estado[var] > val
|
| 74 |
+
elif op == "=":
|
| 75 |
+
mask = metricas_estado[var] == val
|
| 76 |
+
else:
|
| 77 |
+
continue
|
| 78 |
+
|
| 79 |
+
# Usa o índice do DataFrame (que agora são os índices originais dos dados via set_index)
|
| 80 |
+
indices_filtro = metricas_estado[mask].index.tolist()
|
| 81 |
+
indices_outliers.update(indices_filtro)
|
| 82 |
+
|
| 83 |
+
# Converte para inteiros e ordena
|
| 84 |
+
indices_ordenados = sorted([int(i) for i in indices_outliers])
|
| 85 |
+
return ", ".join(map(str, indices_ordenados))
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def adicionar_filtro_callback(n_filtros_atual):
|
| 89 |
+
"""Callback para adicionar um novo filtro (mostra a próxima row oculta).
|
| 90 |
+
|
| 91 |
+
CONTRACT: Retorna 5 itens (n_filtros + 4 row updates).
|
| 92 |
+
Se alterar, atualizar: app.py (outputs de btn_adicionar_filtro.click).
|
| 93 |
+
"""
|
| 94 |
+
n_novo = min(n_filtros_atual + 1, 4)
|
| 95 |
+
|
| 96 |
+
# Retorna: novo estado + 4 updates de visibilidade para rows
|
| 97 |
+
updates = [n_novo]
|
| 98 |
+
for i in range(4):
|
| 99 |
+
updates.append(gr.update(visible=(i < n_novo)))
|
| 100 |
+
|
| 101 |
+
return tuple(updates)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def remover_ultimo_filtro_callback(n_filtros_atual):
|
| 105 |
+
"""Remove o último filtro visível.
|
| 106 |
+
|
| 107 |
+
CONTRACT: Retorna 5 itens (n_filtros + 4 row updates).
|
| 108 |
+
Se alterar, atualizar: app.py (outputs de btn_remover_filtro.click).
|
| 109 |
+
"""
|
| 110 |
+
n_novo = max(0, n_filtros_atual - 1)
|
| 111 |
+
return (n_novo,) + tuple(gr.update(visible=(i < n_novo)) for i in range(4))
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def limpar_filtros_callback():
|
| 115 |
+
"""Limpa todos os filtros e restaura os padrões.
|
| 116 |
+
|
| 117 |
+
CONTRACT: Retorna 18 itens (n_filtros + 4 rows + 4 vars + 4 ops + 4 vals + outliers_texto).
|
| 118 |
+
Se alterar, atualizar: app.py (outputs de btn_limpar_filtros.click).
|
| 119 |
+
"""
|
| 120 |
+
return (
|
| 121 |
+
2, # estado_n_filtros (volta para 2 filtros padrão)
|
| 122 |
+
gr.update(visible=True), # row 0
|
| 123 |
+
gr.update(visible=True), # row 1
|
| 124 |
+
gr.update(visible=False), # row 2
|
| 125 |
+
gr.update(visible=False), # row 3
|
| 126 |
+
gr.update(value="Resíduo Pad."), # var 0
|
| 127 |
+
gr.update(value="Resíduo Pad."), # var 1
|
| 128 |
+
gr.update(value="Resíduo Pad."), # var 2
|
| 129 |
+
gr.update(value="Resíduo Pad."), # var 3
|
| 130 |
+
gr.update(value="<="), # op 0
|
| 131 |
+
gr.update(value=">="), # op 1
|
| 132 |
+
gr.update(value=">="), # op 2
|
| 133 |
+
gr.update(value=">="), # op 3
|
| 134 |
+
gr.update(value=-2.0), # val 0
|
| 135 |
+
gr.update(value=2.0), # val 1
|
| 136 |
+
gr.update(value=0.0), # val 2
|
| 137 |
+
gr.update(value=0.0), # val 3
|
| 138 |
+
"" # outliers_texto
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# ============================================================
|
| 143 |
+
# ITERAÇÃO DE OUTLIERS
|
| 144 |
+
# ============================================================
|
| 145 |
+
|
| 146 |
+
def reiniciar_iteracao_callback(df_original, outliers_anteriores, outliers_texto, reincluir_texto, iteracao_atual, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, grau_min_coef=3, grau_min_f=3):
|
| 147 |
+
"""Combina outliers anteriores com novos (excluindo reincluídos), atualiza estado e reinicia análise.
|
| 148 |
+
Recalcula todas as seções (2 em diante) considerando a ausência dos outliers.
|
| 149 |
+
|
| 150 |
+
CONTRACT: Retorna 30 itens para btn_reiniciar_iteracao.click (app.py:criar_aba).
|
| 151 |
+
Se alterar, atualizar: app.py (outputs de btn_reiniciar_iteracao.click).
|
| 152 |
+
Consome buscar_transformacoes_callback por destructuring (html, resultados, _, *btns).
|
| 153 |
+
Consome atualizar_estatisticas_auto por destructuring (stats, timestamp).
|
| 154 |
+
"""
|
| 155 |
+
# Parse dos novos outliers
|
| 156 |
+
novos_outliers = []
|
| 157 |
+
if outliers_texto and outliers_texto.strip():
|
| 158 |
+
try:
|
| 159 |
+
novos_outliers = [int(x.strip()) for x in outliers_texto.split(",") if x.strip()]
|
| 160 |
+
except:
|
| 161 |
+
pass
|
| 162 |
+
|
| 163 |
+
# Parse dos índices a reincluir
|
| 164 |
+
reincluir = []
|
| 165 |
+
if reincluir_texto and reincluir_texto.strip():
|
| 166 |
+
try:
|
| 167 |
+
reincluir = [int(x.strip()) for x in reincluir_texto.split(",") if x.strip()]
|
| 168 |
+
except:
|
| 169 |
+
pass
|
| 170 |
+
|
| 171 |
+
# Remove reincluídos dos anteriores, depois combina com novos (sem duplicatas)
|
| 172 |
+
anteriores_atualizados = [i for i in (outliers_anteriores or []) if i not in reincluir]
|
| 173 |
+
outliers_combinados = list(set(anteriores_atualizados + novos_outliers))
|
| 174 |
+
outliers_combinados.sort()
|
| 175 |
+
|
| 176 |
+
# Incrementa iteração
|
| 177 |
+
nova_iteracao = (iteracao_atual or 1) + 1
|
| 178 |
+
|
| 179 |
+
# Cria DataFrame filtrado
|
| 180 |
+
df_filtrado = df_original.copy()
|
| 181 |
+
if outliers_combinados:
|
| 182 |
+
df_filtrado = df_filtrado.drop(index=outliers_combinados, errors='ignore')
|
| 183 |
+
|
| 184 |
+
# Recalcula estatísticas (seção 5)
|
| 185 |
+
estatisticas, timestamp = atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x)
|
| 186 |
+
|
| 187 |
+
# Recalcula micronumerosidade (seção 6)
|
| 188 |
+
try:
|
| 189 |
+
resultado_micro = testar_micronumerosidade(df_filtrado, list(colunas_x),
|
| 190 |
+
dicotomicas=dicotomicas, codigo_alocado=codigo_alocado)
|
| 191 |
+
html_micro = formatar_micronumerosidade_html(resultado_micro)
|
| 192 |
+
except Exception as e:
|
| 193 |
+
html_micro = f"<p style='color: red;'>Erro ao calcular micronumerosidade: {e}</p>"
|
| 194 |
+
|
| 195 |
+
# Recalcula gráficos de dispersão originais (seção 7)
|
| 196 |
+
try:
|
| 197 |
+
X = df_filtrado[list(colunas_x)]
|
| 198 |
+
y = df_filtrado[coluna_y]
|
| 199 |
+
print(f"[reiniciar_iteracao] Criando gráfico dispersão com {len(df_filtrado)} pontos (excluídos: {outliers_combinados})")
|
| 200 |
+
fig_dispersao = criar_graficos_dispersao(X, y)
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f"Erro ao gerar gráficos de dispersão: {e}")
|
| 203 |
+
fig_dispersao = None
|
| 204 |
+
|
| 205 |
+
# Recalcula busca de transformações (seção 8)
|
| 206 |
+
try:
|
| 207 |
+
busca_html_result, resultados_busca, _, *btn_updates = buscar_transformacoes_callback(
|
| 208 |
+
df_filtrado, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, int(grau_min_coef), int(grau_min_f)
|
| 209 |
+
)
|
| 210 |
+
except Exception as e:
|
| 211 |
+
busca_html_result = f"<p style='color: red;'>Erro na busca: {e}</p>"
|
| 212 |
+
resultados_busca = []
|
| 213 |
+
btn_updates = [gr.update(visible=False) for _ in range(5)]
|
| 214 |
+
|
| 215 |
+
# Radio buttons coerentes com os novos resultados
|
| 216 |
+
if resultados_busca:
|
| 217 |
+
grau_coef_min = min(
|
| 218 |
+
min(r.get("graus_coef", {}).values(), default=0)
|
| 219 |
+
for r in resultados_busca
|
| 220 |
+
)
|
| 221 |
+
grau_f_min = min(r.get("grau_f", 0) for r in resultados_busca)
|
| 222 |
+
else:
|
| 223 |
+
grau_coef_min = int(grau_min_coef)
|
| 224 |
+
grau_f_min = int(grau_min_f)
|
| 225 |
+
|
| 226 |
+
# Atualiza textos
|
| 227 |
+
html_outliers_ant = formatar_outliers_anteriores_html(
|
| 228 |
+
len(outliers_combinados),
|
| 229 |
+
", ".join(map(str, outliers_combinados)) if outliers_combinados else ""
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
# Visibilidade do accordion
|
| 233 |
+
accordion_visivel = len(outliers_combinados) > 0
|
| 234 |
+
|
| 235 |
+
# Mapa atualizado
|
| 236 |
+
mapa_html = criar_mapa(df_filtrado)
|
| 237 |
+
|
| 238 |
+
# Header updates com timestamp
|
| 239 |
+
header_2_update = gr.update(value=criar_header_secao(2, "Visualizar Dados", timestamp))
|
| 240 |
+
header_5_update = gr.update(value=criar_header_secao(5, "Estatísticas das Variáveis Selecionadas", timestamp))
|
| 241 |
+
header_6_update = gr.update(value=criar_header_secao(6, "Teste de Micronumerosidade (NBR 14.653-2)", timestamp))
|
| 242 |
+
header_7_update = gr.update(value=criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes", timestamp))
|
| 243 |
+
header_8_update = gr.update(value=criar_header_secao(8, "Transformações Sugeridas", timestamp))
|
| 244 |
+
|
| 245 |
+
return (
|
| 246 |
+
outliers_combinados, # estado_outliers_anteriores
|
| 247 |
+
nova_iteracao, # estado_iteracao
|
| 248 |
+
df_filtrado, # estado_df_filtrado
|
| 249 |
+
arredondar_df(df_filtrado), # tabela_dados
|
| 250 |
+
estatisticas, # tabela_estatisticas
|
| 251 |
+
header_5_update, # header_secao_5
|
| 252 |
+
html_outliers_ant, # html_outliers_anteriores
|
| 253 |
+
html_outliers_ant, # html_outliers_sec14
|
| 254 |
+
gr.update(visible=accordion_visivel), # accordion_outliers_anteriores
|
| 255 |
+
"", # outliers_texto (limpo)
|
| 256 |
+
"", # reincluir_texto (limpo)
|
| 257 |
+
None, # tabela_metricas (limpa)
|
| 258 |
+
None, # estado_metricas (limpo)
|
| 259 |
+
gr.update(value=criar_header_secao(13, "Analisar Outliers")), # header_secao_13
|
| 260 |
+
f"Excluídos: {len(outliers_combinados)} | A excluir: 0 | A reincluir: 0 | Total: {len(outliers_combinados)}", # txt_resumo_outliers
|
| 261 |
+
mapa_html, # mapa_html atualizado
|
| 262 |
+
# Novos outputs para seções 2, 6, 7, 8
|
| 263 |
+
header_2_update, # header_secao_2
|
| 264 |
+
html_micro, # html_micronumerosidade
|
| 265 |
+
header_6_update, # header_secao_6
|
| 266 |
+
fig_dispersao, # plot_dispersao
|
| 267 |
+
header_7_update, # header_secao_7
|
| 268 |
+
busca_html_result, # busca_html
|
| 269 |
+
resultados_busca, # estado_resultados_busca
|
| 270 |
+
header_8_update, # header_secao_8
|
| 271 |
+
*btn_updates, # botões adotar (5 botões)
|
| 272 |
+
gr.update(value=grau_coef_min), # slider_grau_coef
|
| 273 |
+
gr.update(value=grau_f_min), # slider_grau_f
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def atualizar_resumo_outliers(outliers_anteriores, outliers_texto, reincluir_texto):
|
| 278 |
+
"""Atualiza o resumo de outliers quando o usuário edita os campos."""
|
| 279 |
+
n_anteriores = len(outliers_anteriores) if outliers_anteriores else 0
|
| 280 |
+
|
| 281 |
+
novos_outliers = []
|
| 282 |
+
if outliers_texto and outliers_texto.strip():
|
| 283 |
+
try:
|
| 284 |
+
novos_outliers = [int(x.strip()) for x in outliers_texto.split(",") if x.strip()]
|
| 285 |
+
except:
|
| 286 |
+
pass
|
| 287 |
+
|
| 288 |
+
reincluir = []
|
| 289 |
+
if reincluir_texto and reincluir_texto.strip():
|
| 290 |
+
try:
|
| 291 |
+
reincluir = [int(x.strip()) for x in reincluir_texto.split(",") if x.strip()]
|
| 292 |
+
except:
|
| 293 |
+
pass
|
| 294 |
+
|
| 295 |
+
n_novos = len(novos_outliers)
|
| 296 |
+
n_reincluir = len(reincluir)
|
| 297 |
+
# Calcula total: anteriores menos reincluídos, mais novos
|
| 298 |
+
anteriores_atualizados = [i for i in (outliers_anteriores or []) if i not in reincluir]
|
| 299 |
+
n_total = len(set(anteriores_atualizados + novos_outliers))
|
| 300 |
+
|
| 301 |
+
return f"Excluídos: {n_anteriores} | A excluir: {n_novos} | A reincluir: {n_reincluir} | Total: {n_total}"
|
backend/app/core/visualizacao/__init__.py
ADDED
|
File without changes
|
backend/app/core/visualizacao/app.py
ADDED
|
@@ -0,0 +1,1477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================
|
| 2 |
+
# IMPORTAÇÕES
|
| 3 |
+
# ============================================================
|
| 4 |
+
import gradio as gr
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import numpy as np
|
| 7 |
+
import folium
|
| 8 |
+
from folium import plugins
|
| 9 |
+
from joblib import load
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
import traceback
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# Importações para gráficos (trazidas de graficos.py)
|
| 16 |
+
from scipy import stats
|
| 17 |
+
import plotly.graph_objects as go
|
| 18 |
+
from statsmodels.stats.outliers_influence import OLSInfluence
|
| 19 |
+
import branca.colormap as cm
|
| 20 |
+
import math
|
| 21 |
+
|
| 22 |
+
from app.core.elaboracao.core import avaliar_imovel, _migrar_pacote_v1_para_v2, exportar_avaliacoes_excel
|
| 23 |
+
from app.core.elaboracao.formatadores import formatar_avaliacao_html
|
| 24 |
+
|
| 25 |
+
# ============================================================
|
| 26 |
+
# CONSTANTES
|
| 27 |
+
# ============================================================
|
| 28 |
+
CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
|
| 29 |
+
|
| 30 |
+
# Constantes para avaliação
|
| 31 |
+
MAX_VARS_AVAL = 20
|
| 32 |
+
N_COLS_AVAL = 2
|
| 33 |
+
N_ROWS_AVAL = MAX_VARS_AVAL // N_COLS_AVAL # 10
|
| 34 |
+
|
| 35 |
+
# Cores consistentes (trazidas de graficos.py)
|
| 36 |
+
COR_PRINCIPAL = '#FF8C00' # Laranja
|
| 37 |
+
COR_LINHA = '#dc3545' # Vermelho para linhas de referência
|
| 38 |
+
|
| 39 |
+
# ============================================================
|
| 40 |
+
# FUNÇÃO: CARREGAR CSS EXTERNO
|
| 41 |
+
# ============================================================
|
| 42 |
+
def carregar_css():
|
| 43 |
+
"""Carrega o arquivo CSS externo."""
|
| 44 |
+
css_path = os.path.join(os.path.dirname(__file__), "styles.css")
|
| 45 |
+
try:
|
| 46 |
+
with open(css_path, "r", encoding="utf-8") as f:
|
| 47 |
+
return f.read()
|
| 48 |
+
except FileNotFoundError:
|
| 49 |
+
print(f"Aviso: Arquivo CSS não encontrado em {css_path}")
|
| 50 |
+
return ""
|
| 51 |
+
|
| 52 |
+
# ============================================================
|
| 53 |
+
# LÓGICA DE GERAÇÃO DE GRÁFICOS (ADAPTADA DE GRAFICOS.PY)
|
| 54 |
+
# ============================================================
|
| 55 |
+
|
| 56 |
+
def _criar_grafico_obs_calc(y_obs, y_calc, indices=None):
|
| 57 |
+
"""Cria gráfico de valores observados vs calculados (Plotly Figure)."""
|
| 58 |
+
try:
|
| 59 |
+
fig = go.Figure()
|
| 60 |
+
|
| 61 |
+
scatter_args = dict(
|
| 62 |
+
x=y_calc,
|
| 63 |
+
y=y_obs,
|
| 64 |
+
mode='markers',
|
| 65 |
+
marker=dict(
|
| 66 |
+
color=COR_PRINCIPAL,
|
| 67 |
+
size=10,
|
| 68 |
+
line=dict(color='black', width=1)
|
| 69 |
+
),
|
| 70 |
+
name='Dados',
|
| 71 |
+
)
|
| 72 |
+
if indices is not None:
|
| 73 |
+
scatter_args['customdata'] = indices
|
| 74 |
+
scatter_args['hovertemplate'] = "<b>Índice:</b> %{customdata}<br><b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>"
|
| 75 |
+
else:
|
| 76 |
+
scatter_args['hovertemplate'] = "<b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>"
|
| 77 |
+
|
| 78 |
+
# Scatter plot
|
| 79 |
+
fig.add_trace(go.Scatter(**scatter_args))
|
| 80 |
+
|
| 81 |
+
# Linha de identidade (45 graus)
|
| 82 |
+
min_val = min(min(y_obs), min(y_calc))
|
| 83 |
+
max_val = max(max(y_obs), max(y_calc))
|
| 84 |
+
margin = (max_val - min_val) * 0.05
|
| 85 |
+
|
| 86 |
+
fig.add_trace(go.Scatter(
|
| 87 |
+
x=[min_val - margin, max_val + margin],
|
| 88 |
+
y=[min_val - margin, max_val + margin],
|
| 89 |
+
mode='lines',
|
| 90 |
+
line=dict(color=COR_LINHA, dash='dash', width=2),
|
| 91 |
+
name='Linha de identidade'
|
| 92 |
+
))
|
| 93 |
+
|
| 94 |
+
fig.update_layout(
|
| 95 |
+
title=dict(text='Valores Observados vs Calculados', x=0.5),
|
| 96 |
+
xaxis_title='Valores Calculados',
|
| 97 |
+
yaxis_title='Valores Observados',
|
| 98 |
+
showlegend=True,
|
| 99 |
+
plot_bgcolor='white',
|
| 100 |
+
margin=dict(l=60, r=40, t=60, b=60)
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 104 |
+
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 105 |
+
|
| 106 |
+
return fig
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"Erro ao criar gráfico obs vs calc: {e}")
|
| 109 |
+
return None
|
| 110 |
+
|
| 111 |
+
def _criar_grafico_residuos(y_calc, residuos, indices=None):
|
| 112 |
+
"""Cria gráfico de resíduos vs valores ajustados (Plotly Figure)."""
|
| 113 |
+
try:
|
| 114 |
+
fig = go.Figure()
|
| 115 |
+
|
| 116 |
+
scatter_args = dict(
|
| 117 |
+
x=y_calc,
|
| 118 |
+
y=residuos,
|
| 119 |
+
mode='markers',
|
| 120 |
+
marker=dict(
|
| 121 |
+
color=COR_PRINCIPAL,
|
| 122 |
+
size=10,
|
| 123 |
+
line=dict(color='black', width=1)
|
| 124 |
+
),
|
| 125 |
+
name='Resíduos',
|
| 126 |
+
)
|
| 127 |
+
if indices is not None:
|
| 128 |
+
scatter_args['customdata'] = indices
|
| 129 |
+
scatter_args['hovertemplate'] = "<b>Índice:</b> %{customdata}<br><b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>"
|
| 130 |
+
else:
|
| 131 |
+
scatter_args['hovertemplate'] = "<b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>"
|
| 132 |
+
|
| 133 |
+
fig.add_trace(go.Scatter(**scatter_args))
|
| 134 |
+
|
| 135 |
+
fig.add_hline(y=0, line_dash="dash", line_color=COR_LINHA, line_width=2)
|
| 136 |
+
|
| 137 |
+
fig.update_layout(
|
| 138 |
+
title=dict(text='Resíduos vs Valores Ajustados', x=0.5),
|
| 139 |
+
xaxis_title='Valores Ajustados',
|
| 140 |
+
yaxis_title='Resíduos',
|
| 141 |
+
showlegend=False,
|
| 142 |
+
plot_bgcolor='white',
|
| 143 |
+
margin=dict(l=60, r=40, t=60, b=60)
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 147 |
+
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 148 |
+
|
| 149 |
+
return fig
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f"Erro ao criar gráfico de resíduos: {e}")
|
| 152 |
+
return None
|
| 153 |
+
|
| 154 |
+
def _criar_histograma_residuos(residuos):
|
| 155 |
+
"""Cria histograma dos resíduos com curva normal (Plotly Figure)."""
|
| 156 |
+
try:
|
| 157 |
+
fig = go.Figure()
|
| 158 |
+
|
| 159 |
+
fig.add_trace(go.Histogram(
|
| 160 |
+
x=residuos,
|
| 161 |
+
histnorm='probability density',
|
| 162 |
+
marker=dict(color=COR_PRINCIPAL, line=dict(color='black', width=1)),
|
| 163 |
+
opacity=0.7,
|
| 164 |
+
name='Resíduos'
|
| 165 |
+
))
|
| 166 |
+
|
| 167 |
+
mu, sigma = np.mean(residuos), np.std(residuos)
|
| 168 |
+
x_norm = np.linspace(min(residuos) - sigma, max(residuos) + sigma, 100)
|
| 169 |
+
y_norm = stats.norm.pdf(x_norm, mu, sigma)
|
| 170 |
+
|
| 171 |
+
fig.add_trace(go.Scatter(
|
| 172 |
+
x=x_norm,
|
| 173 |
+
y=y_norm,
|
| 174 |
+
mode='lines',
|
| 175 |
+
line=dict(color=COR_LINHA, width=3),
|
| 176 |
+
name='Curva Normal'
|
| 177 |
+
))
|
| 178 |
+
|
| 179 |
+
fig.update_layout(
|
| 180 |
+
title=dict(text='Distribuição dos Resíduos', x=0.5),
|
| 181 |
+
xaxis_title='Resíduos',
|
| 182 |
+
yaxis_title='Densidade',
|
| 183 |
+
showlegend=True,
|
| 184 |
+
plot_bgcolor='white',
|
| 185 |
+
barmode='overlay',
|
| 186 |
+
margin=dict(l=60, r=40, t=60, b=60)
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 190 |
+
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 191 |
+
|
| 192 |
+
return fig
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(f"Erro ao criar histograma: {e}")
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _criar_grafico_cook(modelos_sm):
|
| 199 |
+
"""Cria gráfico de Distância de Cook (Plotly Figure)."""
|
| 200 |
+
try:
|
| 201 |
+
if modelos_sm is None: return None
|
| 202 |
+
|
| 203 |
+
influence = OLSInfluence(modelos_sm)
|
| 204 |
+
cooks_d = influence.cooks_distance[0]
|
| 205 |
+
|
| 206 |
+
n = len(cooks_d)
|
| 207 |
+
indices = np.arange(1, n + 1)
|
| 208 |
+
limite = 4 / n
|
| 209 |
+
|
| 210 |
+
fig = go.Figure()
|
| 211 |
+
|
| 212 |
+
# Hastes (linhas verticais)
|
| 213 |
+
for idx, valor in zip(indices, cooks_d):
|
| 214 |
+
cor = COR_LINHA if valor > limite else COR_PRINCIPAL
|
| 215 |
+
fig.add_trace(
|
| 216 |
+
go.Scatter(x=[idx, idx],
|
| 217 |
+
y=[0, valor],
|
| 218 |
+
mode='lines',
|
| 219 |
+
line=dict(color=cor, width=1.5),
|
| 220 |
+
showlegend=False,
|
| 221 |
+
hoverinfo='skip'))
|
| 222 |
+
|
| 223 |
+
# Pontos
|
| 224 |
+
cores_pontos = [
|
| 225 |
+
COR_LINHA if v > limite else COR_PRINCIPAL for v in cooks_d
|
| 226 |
+
]
|
| 227 |
+
fig.add_trace(
|
| 228 |
+
go.Scatter(
|
| 229 |
+
x=indices,
|
| 230 |
+
y=cooks_d,
|
| 231 |
+
mode='markers',
|
| 232 |
+
marker=dict(color=cores_pontos,
|
| 233 |
+
size=8,
|
| 234 |
+
line=dict(color='black', width=1)),
|
| 235 |
+
name='Distância de Cook',
|
| 236 |
+
hovertemplate='Obs: %{x}<br>Cook: %{y:.4f}<extra></extra>'))
|
| 237 |
+
|
| 238 |
+
fig.add_hline(y=limite,
|
| 239 |
+
line_dash="dash",
|
| 240 |
+
line_color='gray',
|
| 241 |
+
annotation_text=f"4/n = {limite:.4f}",
|
| 242 |
+
annotation_position="top right")
|
| 243 |
+
|
| 244 |
+
fig.update_layout(title=dict(text='Distância de Cook', x=0.5),
|
| 245 |
+
xaxis_title='Observação',
|
| 246 |
+
yaxis_title='Distância de Cook',
|
| 247 |
+
plot_bgcolor='white',
|
| 248 |
+
margin=dict(l=60, r=40, t=60, b=60))
|
| 249 |
+
|
| 250 |
+
fig.update_xaxes(showgrid=True,
|
| 251 |
+
gridcolor='lightgray',
|
| 252 |
+
showline=True,
|
| 253 |
+
linecolor='black')
|
| 254 |
+
fig.update_yaxes(showgrid=True,
|
| 255 |
+
gridcolor='lightgray',
|
| 256 |
+
showline=True,
|
| 257 |
+
linecolor='black')
|
| 258 |
+
|
| 259 |
+
return fig
|
| 260 |
+
except Exception as e:
|
| 261 |
+
print(f"Erro ao criar gráfico de Cook: {e}")
|
| 262 |
+
return None
|
| 263 |
+
|
| 264 |
+
def _criar_grafico_correlacao(modelos_sm):
|
| 265 |
+
"""Gera heatmap de correlação com cores customizadas e diagonal limpa."""
|
| 266 |
+
try:
|
| 267 |
+
if modelos_sm is None or not hasattr(modelos_sm, 'model'):
|
| 268 |
+
return None
|
| 269 |
+
|
| 270 |
+
model = modelos_sm.model
|
| 271 |
+
X = model.exog
|
| 272 |
+
X_names = model.exog_names
|
| 273 |
+
y = model.endog
|
| 274 |
+
y_name = getattr(model, 'endog_names', 'Variável Dependente')
|
| 275 |
+
|
| 276 |
+
df_X = pd.DataFrame(X, columns=X_names)
|
| 277 |
+
df_X = df_X.drop(
|
| 278 |
+
columns=[c for c in df_X.columns if str(c).lower() in ('const', 'intercept')],
|
| 279 |
+
errors='ignore'
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
df_y = pd.DataFrame({y_name: y})
|
| 283 |
+
df = pd.concat([df_y, df_X], axis=1)
|
| 284 |
+
|
| 285 |
+
# garantir numérico
|
| 286 |
+
df = df.apply(pd.to_numeric, errors='coerce')
|
| 287 |
+
|
| 288 |
+
# remover colunas constantes
|
| 289 |
+
variancias = df.var(ddof=0)
|
| 290 |
+
df = df.loc[:, variancias.fillna(0) > 0]
|
| 291 |
+
|
| 292 |
+
if df.shape[1] < 2:
|
| 293 |
+
return None
|
| 294 |
+
|
| 295 |
+
# ✅ CRIAR corr
|
| 296 |
+
corr = df.corr()
|
| 297 |
+
|
| 298 |
+
if corr.isnull().values.all():
|
| 299 |
+
return None
|
| 300 |
+
|
| 301 |
+
# ✅ remover diagonal (SEM numpy in-place)
|
| 302 |
+
mask = np.eye(len(corr), dtype=bool)
|
| 303 |
+
corr = corr.where(~mask)
|
| 304 |
+
|
| 305 |
+
# texto
|
| 306 |
+
text = np.where(
|
| 307 |
+
np.isnan(corr.values),
|
| 308 |
+
"",
|
| 309 |
+
np.round(corr.values, 2).astype(str)
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
fig = go.Figure(go.Heatmap(
|
| 313 |
+
z=corr.values,
|
| 314 |
+
x=corr.columns,
|
| 315 |
+
y=corr.index,
|
| 316 |
+
text=text,
|
| 317 |
+
texttemplate="%{text}",
|
| 318 |
+
textfont=dict(size=10),
|
| 319 |
+
zmin=-1,
|
| 320 |
+
zmax=1,
|
| 321 |
+
zmid=0,
|
| 322 |
+
colorscale = [
|
| 323 |
+
[0.00, "rgb(103,0,31)"],
|
| 324 |
+
[0.08, "rgb(178,24,43)"],
|
| 325 |
+
[0.16, "rgb(214,96,77)"],
|
| 326 |
+
[0.24, "rgb(244,165,130)"],
|
| 327 |
+
[0.32, "rgb(253,219,199)"],
|
| 328 |
+
|
| 329 |
+
# faixa branca larga (inalterada)
|
| 330 |
+
[0.45, "rgb(255,255,255)"],
|
| 331 |
+
[0.55, "rgb(255,255,255)"],
|
| 332 |
+
|
| 333 |
+
[0.68, "rgb(209,229,240)"],
|
| 334 |
+
[0.76, "rgb(146,197,222)"],
|
| 335 |
+
[0.84, "rgb(67,147,195)"],
|
| 336 |
+
[0.92, "rgb(33,102,172)"],
|
| 337 |
+
[1.00, "rgb(5,48,97)"]
|
| 338 |
+
],
|
| 339 |
+
colorbar=dict(title='Correlação'),
|
| 340 |
+
hovertemplate="%{x} × %{y}<br>ρ = %{z:.3f}<extra></extra>"
|
| 341 |
+
))
|
| 342 |
+
|
| 343 |
+
# ✅ diagonal VISUAL (robusta)
|
| 344 |
+
fig.add_shape(
|
| 345 |
+
type="line",
|
| 346 |
+
xref="paper",
|
| 347 |
+
yref="paper",
|
| 348 |
+
x0=0, y0=1,
|
| 349 |
+
x1=1, y1=0,
|
| 350 |
+
line=dict(color="rgba(0,0,0,0.35)", width=1),
|
| 351 |
+
layer="above"
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
fig.update_layout(
|
| 355 |
+
title=dict(text="Matriz de Correlação", x=0.5),
|
| 356 |
+
height=600,
|
| 357 |
+
template='plotly_white',
|
| 358 |
+
xaxis=dict(tickangle=45, showgrid=False),
|
| 359 |
+
yaxis=dict(autorange='reversed', showgrid=False)
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
return fig
|
| 363 |
+
|
| 364 |
+
except Exception as e:
|
| 365 |
+
print(f"Erro na geração do gráfico: {e}")
|
| 366 |
+
traceback.print_exc()
|
| 367 |
+
return None
|
| 368 |
+
|
| 369 |
+
# def _criar_grafico_correlacao(modelos_sm):
|
| 370 |
+
# """Gera heatmap de correlação (Plotly Figure)."""
|
| 371 |
+
# try:
|
| 372 |
+
# if modelos_sm is None or not hasattr(modelos_sm, 'model'):
|
| 373 |
+
# return None
|
| 374 |
+
|
| 375 |
+
# model = modelos_sm.model
|
| 376 |
+
# if not hasattr(model, 'exog') or not hasattr(model, 'endog'):
|
| 377 |
+
# return None
|
| 378 |
+
|
| 379 |
+
# X = model.exog
|
| 380 |
+
# X_names = model.exog_names
|
| 381 |
+
# y = model.endog
|
| 382 |
+
# y_name = getattr(model, 'endog_names', 'Variável Dependente')
|
| 383 |
+
|
| 384 |
+
# df_X = pd.DataFrame(X, columns=X_names)
|
| 385 |
+
|
| 386 |
+
# # Remover constantes explícitas
|
| 387 |
+
# df_X = df_X.drop(
|
| 388 |
+
# columns=[c for c in df_X.columns if c.lower() in ('const', 'intercept')],
|
| 389 |
+
# errors='ignore'
|
| 390 |
+
# )
|
| 391 |
+
|
| 392 |
+
# df_y = pd.DataFrame({y_name: y})
|
| 393 |
+
# df = pd.concat([df_y, df_X], axis=1)
|
| 394 |
+
|
| 395 |
+
# # Remover colunas constantes
|
| 396 |
+
# variancias = df.var(ddof=0)
|
| 397 |
+
# df = df.loc[:, variancias.fillna(0) > 0]
|
| 398 |
+
|
| 399 |
+
# if df.shape[1] < 2: return None
|
| 400 |
+
|
| 401 |
+
# corr = df.corr()
|
| 402 |
+
# if corr.isnull().values.all(): return None
|
| 403 |
+
|
| 404 |
+
# text = np.round(corr.values, 2).astype(str)
|
| 405 |
+
|
| 406 |
+
# fig = go.Figure(data=go.Heatmap(
|
| 407 |
+
# z=corr.values,
|
| 408 |
+
# x=corr.columns,
|
| 409 |
+
# y=corr.index,
|
| 410 |
+
# text=text,
|
| 411 |
+
# texttemplate="%{text}",
|
| 412 |
+
# textfont=dict(size=10),
|
| 413 |
+
# zmin=-1, zmax=1,
|
| 414 |
+
# colorscale = [
|
| 415 |
+
# [0.0, "rgb(178,24,43)"], # red
|
| 416 |
+
# [0.35, "rgb(178,24,43)"],
|
| 417 |
+
|
| 418 |
+
# [0.45, "rgb(255,255,255)"], # start white
|
| 419 |
+
# [0.55, "rgb(255,255,255)"], # end white
|
| 420 |
+
|
| 421 |
+
# [0.65, "rgb(33,102,172)"],
|
| 422 |
+
# [1.0, "rgb(33,102,172)"] # blue
|
| 423 |
+
# ],
|
| 424 |
+
# colorbar=dict(title='Correlação')
|
| 425 |
+
# ))
|
| 426 |
+
|
| 427 |
+
# fig.update_layout(
|
| 428 |
+
# title=dict(text="Matriz de Correlação", x=0.5),
|
| 429 |
+
# height=600,
|
| 430 |
+
# template='plotly_white',
|
| 431 |
+
# xaxis=dict(tickangle=45, showgrid=False),
|
| 432 |
+
# yaxis=dict(autorange='reversed', showgrid=True)
|
| 433 |
+
# )
|
| 434 |
+
|
| 435 |
+
# return fig
|
| 436 |
+
# except Exception:
|
| 437 |
+
# return None
|
| 438 |
+
|
| 439 |
+
def gerar_todos_graficos(pacote):
|
| 440 |
+
"""Orquestra a geração de todos os gráficos a partir do pacote."""
|
| 441 |
+
graficos = {
|
| 442 |
+
"obs_calc": None,
|
| 443 |
+
"residuos": None,
|
| 444 |
+
"hist": None,
|
| 445 |
+
"cook": None,
|
| 446 |
+
"corr": None
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
obs_calc = pacote["modelo"]["obs_calc"]
|
| 450 |
+
modelos_sm = pacote["modelo"]["sm"]
|
| 451 |
+
|
| 452 |
+
# Identificar vetores
|
| 453 |
+
y_obs = None
|
| 454 |
+
y_calc = None
|
| 455 |
+
residuos = None
|
| 456 |
+
indices = None
|
| 457 |
+
|
| 458 |
+
# Tenta pegar do DataFrame obs_calc
|
| 459 |
+
if obs_calc is not None and not obs_calc.empty:
|
| 460 |
+
cols_lower = {str(c).lower(): c for c in obs_calc.columns}
|
| 461 |
+
|
| 462 |
+
# Extrair índices do DataFrame
|
| 463 |
+
indices = obs_calc.index.values if obs_calc.index is not None else None
|
| 464 |
+
|
| 465 |
+
# Encontrar coluna observada
|
| 466 |
+
for nome in ['observado', 'obs', 'y_obs', 'y', 'valor_observado']:
|
| 467 |
+
if nome in cols_lower:
|
| 468 |
+
y_obs = obs_calc[cols_lower[nome]].values
|
| 469 |
+
break
|
| 470 |
+
|
| 471 |
+
# Encontrar coluna calculada
|
| 472 |
+
for nome in ['calculado', 'calc', 'y_calc', 'y_hat', 'previsto']:
|
| 473 |
+
if nome in cols_lower:
|
| 474 |
+
y_calc = obs_calc[cols_lower[nome]].values
|
| 475 |
+
break
|
| 476 |
+
|
| 477 |
+
# Encontrar coluna resíduos
|
| 478 |
+
for nome in ['residuo', 'residuos', 'resid']:
|
| 479 |
+
if nome in cols_lower:
|
| 480 |
+
residuos = obs_calc[cols_lower[nome]].values
|
| 481 |
+
break
|
| 482 |
+
|
| 483 |
+
# Fallback para o objeto statsmodels
|
| 484 |
+
if modelos_sm is not None:
|
| 485 |
+
try:
|
| 486 |
+
if y_obs is None and hasattr(modelos_sm.model, 'endog'):
|
| 487 |
+
y_obs = modelos_sm.model.endog
|
| 488 |
+
if y_calc is None and hasattr(modelos_sm, 'fittedvalues'):
|
| 489 |
+
y_calc = modelos_sm.fittedvalues
|
| 490 |
+
if residuos is None and hasattr(modelos_sm, 'resid'):
|
| 491 |
+
residuos = modelos_sm.resid
|
| 492 |
+
except Exception:
|
| 493 |
+
pass
|
| 494 |
+
|
| 495 |
+
# Cálculo manual se necessário
|
| 496 |
+
if residuos is None and y_obs is not None and y_calc is not None:
|
| 497 |
+
residuos = np.array(y_obs) - np.array(y_calc)
|
| 498 |
+
|
| 499 |
+
# Converter para numpy
|
| 500 |
+
y_obs = np.array(y_obs) if y_obs is not None else None
|
| 501 |
+
y_calc = np.array(y_calc) if y_calc is not None else None
|
| 502 |
+
residuos = np.array(residuos) if residuos is not None else None
|
| 503 |
+
|
| 504 |
+
# Gerar cada gráfico
|
| 505 |
+
if y_obs is not None and y_calc is not None:
|
| 506 |
+
graficos["obs_calc"] = _criar_grafico_obs_calc(y_obs, y_calc, indices)
|
| 507 |
+
|
| 508 |
+
if residuos is not None and y_calc is not None:
|
| 509 |
+
graficos["residuos"] = _criar_grafico_residuos(y_calc, residuos, indices)
|
| 510 |
+
|
| 511 |
+
if residuos is not None:
|
| 512 |
+
graficos["hist"] = _criar_histograma_residuos(residuos)
|
| 513 |
+
|
| 514 |
+
if modelos_sm is not None:
|
| 515 |
+
graficos["cook"] = _criar_grafico_cook(modelos_sm)
|
| 516 |
+
graficos["corr"] = _criar_grafico_correlacao(modelos_sm)
|
| 517 |
+
|
| 518 |
+
return graficos
|
| 519 |
+
|
| 520 |
+
# ============================================================
|
| 521 |
+
# FUNÇÃO: REORGANIZAR DIAGNOSTICOS PARA EXIBIÇÃO
|
| 522 |
+
# ============================================================
|
| 523 |
+
def reorganizar_modelos_resumos(diagnosticos):
|
| 524 |
+
"""
|
| 525 |
+
Converte diagnosticos v2 (já agrupado) para formato de exibição HTML.
|
| 526 |
+
"""
|
| 527 |
+
gerais = diagnosticos.get("gerais", {})
|
| 528 |
+
return {
|
| 529 |
+
"estatisticas_gerais": {
|
| 530 |
+
"n": {"nome": "Número de observações", "valor": gerais.get("n")},
|
| 531 |
+
"k": {"nome": "Número de variáveis independentes", "valor": gerais.get("k")},
|
| 532 |
+
"desvio_padrao_residuos": {"nome": "Desvio padrão dos resíduos", "valor": gerais.get("desvio_padrao_residuos")},
|
| 533 |
+
"mse": {"nome": "MSE", "valor": gerais.get("mse")},
|
| 534 |
+
"r2": {"nome": "R²", "valor": gerais.get("r2")},
|
| 535 |
+
"r2_ajustado": {"nome": "R² ajustado", "valor": gerais.get("r2_ajustado")},
|
| 536 |
+
"r_pearson": {"nome": "Correlação Pearson", "valor": gerais.get("r_pearson")}
|
| 537 |
+
},
|
| 538 |
+
"teste_f": {
|
| 539 |
+
"nome": "Teste F",
|
| 540 |
+
"estatistica": diagnosticos.get("teste_f", {}).get("estatistica"),
|
| 541 |
+
"pvalor": diagnosticos.get("teste_f", {}).get("p_valor"),
|
| 542 |
+
"interpretacao": diagnosticos.get("teste_f", {}).get("interpretacao")
|
| 543 |
+
},
|
| 544 |
+
"teste_ks": {
|
| 545 |
+
"nome": "Teste de Normalidade (Kolmogorov-Smirnov)",
|
| 546 |
+
"estatistica": diagnosticos.get("teste_ks", {}).get("estatistica"),
|
| 547 |
+
"pvalor": diagnosticos.get("teste_ks", {}).get("p_valor"),
|
| 548 |
+
"interpretacao": diagnosticos.get("teste_ks", {}).get("interpretacao")
|
| 549 |
+
},
|
| 550 |
+
"perc_resid": {
|
| 551 |
+
"nome": "Teste de Normalidade (Comparação com a Curva Normal)",
|
| 552 |
+
"valor": diagnosticos.get("teste_normalidade", {}).get("percentuais"),
|
| 553 |
+
"interpretacao": [
|
| 554 |
+
"Ideal 68% → aceitável entre 64% e 75%",
|
| 555 |
+
"Ideal 90% → aceitável entre 88% e 95%",
|
| 556 |
+
"Ideal 95% → aceitável entre 95% e 100%"
|
| 557 |
+
]
|
| 558 |
+
},
|
| 559 |
+
"teste_dw": {
|
| 560 |
+
"nome": "Teste de Autocorrelação (Durbin-Watson)",
|
| 561 |
+
"estatistica": diagnosticos.get("teste_dw", {}).get("estatistica"),
|
| 562 |
+
"interpretacao": diagnosticos.get("teste_dw", {}).get("interpretacao")
|
| 563 |
+
},
|
| 564 |
+
"teste_bp": {
|
| 565 |
+
"nome": "Teste de Homocedasticidade (Breusch-Pagan)",
|
| 566 |
+
"estatistica": diagnosticos.get("teste_bp", {}).get("estatistica"),
|
| 567 |
+
"pvalor": diagnosticos.get("teste_bp", {}).get("p_valor"),
|
| 568 |
+
"interpretacao": diagnosticos.get("teste_bp", {}).get("interpretacao")
|
| 569 |
+
},
|
| 570 |
+
"equacao": diagnosticos.get("equacao")
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
# ============================================================
|
| 574 |
+
# FUNÇÃO: FORMATAR VALOR MONETÁRIO
|
| 575 |
+
# ============================================================
|
| 576 |
+
def formatar_monetario(valor):
|
| 577 |
+
if pd.isna(valor): return "N/A"
|
| 578 |
+
return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 579 |
+
|
| 580 |
+
# ============================================================
|
| 581 |
+
# FUNÇÃO: FORMATAR RESUMO COMO HTML
|
| 582 |
+
# ============================================================
|
| 583 |
+
def formatar_resumo_html(resumo_reorganizado):
|
| 584 |
+
|
| 585 |
+
def criar_titulo_secao(titulo):
|
| 586 |
+
return f'<div class="section-title-orange-solid">{titulo}</div>'
|
| 587 |
+
|
| 588 |
+
def criar_linha_campo(campo, valor):
|
| 589 |
+
return f"""<div class="field-row"><span class="field-row-label">{campo}</span><span class="field-row-value">{valor}</span></div>"""
|
| 590 |
+
|
| 591 |
+
def criar_linha_interpretacao(interpretacao):
|
| 592 |
+
return f"""<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value-italic">{interpretacao}</span></div>"""
|
| 593 |
+
|
| 594 |
+
def formatar_numero(valor, casas_decimais=4):
|
| 595 |
+
if valor is None: return "N/A"
|
| 596 |
+
if isinstance(valor, (int, float, np.floating)): return f"{valor:.{casas_decimais}f}"
|
| 597 |
+
return str(valor)
|
| 598 |
+
|
| 599 |
+
linhas_html = []
|
| 600 |
+
|
| 601 |
+
# 1. Estatísticas Gerais
|
| 602 |
+
estat_gerais = resumo_reorganizado.get("estatisticas_gerais", {})
|
| 603 |
+
if estat_gerais:
|
| 604 |
+
linhas_html.append(criar_titulo_secao("Estatísticas Gerais"))
|
| 605 |
+
for chave, dados in estat_gerais.items():
|
| 606 |
+
linhas_html.append(criar_linha_campo(dados.get("nome", chave), formatar_numero(dados.get("valor"))))
|
| 607 |
+
|
| 608 |
+
# 2. Testes (ordem: F, KS, Curva Normal, DW, BP)
|
| 609 |
+
for chave_teste, label in [("teste_f", "Estatística F"), ("teste_ks", "Estatística KS")]:
|
| 610 |
+
teste = resumo_reorganizado.get(chave_teste, {})
|
| 611 |
+
if teste.get("estatistica") is not None:
|
| 612 |
+
linhas_html.append(criar_titulo_secao(teste.get("nome")))
|
| 613 |
+
linhas_html.append(criar_linha_campo(label, formatar_numero(teste["estatistica"])))
|
| 614 |
+
if teste.get("pvalor") is not None:
|
| 615 |
+
linhas_html.append(criar_linha_campo("P-valor", formatar_numero(teste["pvalor"])))
|
| 616 |
+
if teste.get("interpretacao"):
|
| 617 |
+
linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
|
| 618 |
+
|
| 619 |
+
# Percentuais (Comparação com a Curva Normal — logo após KS)
|
| 620 |
+
perc_resid = resumo_reorganizado.get("perc_resid", {})
|
| 621 |
+
if perc_resid.get("valor") is not None:
|
| 622 |
+
linhas_html.append(criar_titulo_secao(perc_resid.get("nome")))
|
| 623 |
+
linhas_html.append(criar_linha_campo("Percentuais Atingidos", perc_resid["valor"]))
|
| 624 |
+
if perc_resid.get("interpretacao"):
|
| 625 |
+
linhas_html.append('<div class="interpretation-label">Interpretação</div>')
|
| 626 |
+
for ideal in perc_resid["interpretacao"]:
|
| 627 |
+
linhas_html.append(f'<div class="interpretation-item">• {ideal}</div>')
|
| 628 |
+
|
| 629 |
+
for chave_teste, label in [("teste_dw", "Estatística DW"), ("teste_bp", "Estatística LM")]:
|
| 630 |
+
teste = resumo_reorganizado.get(chave_teste, {})
|
| 631 |
+
if teste.get("estatistica") is not None:
|
| 632 |
+
linhas_html.append(criar_titulo_secao(teste.get("nome")))
|
| 633 |
+
linhas_html.append(criar_linha_campo(label, formatar_numero(teste["estatistica"])))
|
| 634 |
+
if teste.get("pvalor") is not None:
|
| 635 |
+
linhas_html.append(criar_linha_campo("P-valor", formatar_numero(teste["pvalor"])))
|
| 636 |
+
if teste.get("interpretacao"):
|
| 637 |
+
linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
|
| 638 |
+
|
| 639 |
+
# Equação
|
| 640 |
+
equacao = resumo_reorganizado.get("equacao")
|
| 641 |
+
if equacao:
|
| 642 |
+
linhas_html.append(criar_titulo_secao("Equação do Modelo"))
|
| 643 |
+
linhas_html.append(f'<div class="equation-box">{equacao}</div>')
|
| 644 |
+
|
| 645 |
+
return f"""<div class="dai-card scrollable-container">{"".join(linhas_html)}</div>"""
|
| 646 |
+
|
| 647 |
+
# ============================================================
|
| 648 |
+
# FUNÇÃO: CRIAR TÍTULO DE SEÇÃO ESTILIZADO
|
| 649 |
+
# ============================================================
|
| 650 |
+
def criar_titulo_secao_html(titulo):
|
| 651 |
+
return f'<div class="section-title-orange">{titulo}</div>'
|
| 652 |
+
|
| 653 |
+
# ============================================================
|
| 654 |
+
# FUNÇÃO: FORMATAR ESCALAS/TRANSFORMAÇÕES
|
| 655 |
+
# ============================================================
|
| 656 |
+
def formatar_escalas_html(escalas_raw):
|
| 657 |
+
if isinstance(escalas_raw, pd.DataFrame): itens = escalas_raw.iloc[:, 0].tolist()
|
| 658 |
+
elif isinstance(escalas_raw, list): itens = escalas_raw
|
| 659 |
+
else: itens = [str(escalas_raw)]
|
| 660 |
+
|
| 661 |
+
itens = [str(item) for item in itens if item and str(item).strip()]
|
| 662 |
+
if not itens: return "<p style='color: #6c757d; font-style: italic;'>Nenhuma transformação disponível.</p>"
|
| 663 |
+
|
| 664 |
+
max_chars = max(len(item) for item in itens)
|
| 665 |
+
largura_min = max(150, max_chars * 7 + 28)
|
| 666 |
+
cards_html = ""
|
| 667 |
+
for item in itens:
|
| 668 |
+
if ":" in item:
|
| 669 |
+
partes = item.split(":", 1)
|
| 670 |
+
conteudo = f"""<span style="font-weight: 600; color: #495057;">{partes[0].strip()}:</span><span style="font-weight: 400; color: #6c757d; margin-left: 4px;">{partes[1].strip()}</span>"""
|
| 671 |
+
else:
|
| 672 |
+
conteudo = f"""<span style="font-weight: 600; color: #495057;">{item}</span>"""
|
| 673 |
+
cards_html += f"""<div class="dai-card-light" style="min-width: {largura_min}px;">{conteudo}</div>"""
|
| 674 |
+
|
| 675 |
+
return f"""<div class="dai-card">{criar_titulo_secao_html("Escalas / Transformações")}<div class="dai-cards-grid" style="grid-template-columns: repeat(auto-fill, minmax({largura_min}px, 1fr));">{cards_html}</div></div>"""
|
| 676 |
+
|
| 677 |
+
# ============================================================
|
| 678 |
+
# FUNÇÃO: GERAR MAPA FOLIUM (com suporte a dimensionamento por variável)
|
| 679 |
+
# ============================================================
|
| 680 |
+
def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None, col_y=None):
|
| 681 |
+
"""
|
| 682 |
+
Cria mapa Folium com os dados, com suporte a dimensionamento proporcional.
|
| 683 |
+
|
| 684 |
+
Parâmetros:
|
| 685 |
+
df: DataFrame com os dados
|
| 686 |
+
lat_col: nome da coluna de latitude
|
| 687 |
+
lon_col: nome da coluna de longitude
|
| 688 |
+
cor_col: coluna para colorir os pontos (opcional)
|
| 689 |
+
tamanho_col: coluna numérica para dimensionar os círculos (opcional)
|
| 690 |
+
|
| 691 |
+
Retorna:
|
| 692 |
+
HTML do mapa
|
| 693 |
+
"""
|
| 694 |
+
# Verifica se colunas existem
|
| 695 |
+
cols_lower = {str(c).lower(): c for c in df.columns}
|
| 696 |
+
|
| 697 |
+
lat_real = None
|
| 698 |
+
lon_real = None
|
| 699 |
+
|
| 700 |
+
for nome in ["lat", "latitude", "siat_latitude"]:
|
| 701 |
+
if nome in cols_lower:
|
| 702 |
+
lat_real = cols_lower[nome]
|
| 703 |
+
break
|
| 704 |
+
|
| 705 |
+
for nome in ["lon", "longitude", "long", "siat_longitude"]:
|
| 706 |
+
if nome in cols_lower:
|
| 707 |
+
lon_real = cols_lower[nome]
|
| 708 |
+
break
|
| 709 |
+
|
| 710 |
+
if lat_real is None or lon_real is None:
|
| 711 |
+
return "<p>Coordenadas (lat/lon) não encontradas nos dados.</p>"
|
| 712 |
+
|
| 713 |
+
# Filtra dados válidos
|
| 714 |
+
df_mapa = df.dropna(subset=[lat_real, lon_real]).copy()
|
| 715 |
+
if df_mapa.empty:
|
| 716 |
+
return "<p>Sem coordenadas válidas para exibir.</p>"
|
| 717 |
+
|
| 718 |
+
# Cria mapa
|
| 719 |
+
centro_lat = df_mapa[lat_real].mean()
|
| 720 |
+
centro_lon = df_mapa[lon_real].mean()
|
| 721 |
+
|
| 722 |
+
m = folium.Map(
|
| 723 |
+
location=[centro_lat, centro_lon],
|
| 724 |
+
zoom_start=12,
|
| 725 |
+
tiles=None
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
# Camadas base
|
| 729 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True).add_to(m)
|
| 730 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True).add_to(m)
|
| 731 |
+
|
| 732 |
+
# Se tamanho_col fornecido mas cor_col não, usa mesma variável para cor
|
| 733 |
+
if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col:
|
| 734 |
+
cor_col = tamanho_col
|
| 735 |
+
|
| 736 |
+
# Colormap se houver coluna de cor (verde → vermelho)
|
| 737 |
+
colormap = None
|
| 738 |
+
if cor_col and cor_col in df_mapa.columns:
|
| 739 |
+
vmin = df_mapa[cor_col].min()
|
| 740 |
+
vmax = df_mapa[cor_col].max()
|
| 741 |
+
colormap = cm.LinearColormap(
|
| 742 |
+
colors=["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
|
| 743 |
+
vmin=vmin,
|
| 744 |
+
vmax=vmax,
|
| 745 |
+
caption=cor_col
|
| 746 |
+
)
|
| 747 |
+
colormap.add_to(m)
|
| 748 |
+
|
| 749 |
+
# Escala de tamanho proporcional
|
| 750 |
+
raio_min, raio_max = 3, 18
|
| 751 |
+
tamanho_func = None
|
| 752 |
+
if tamanho_col and tamanho_col != "Visualização Padrão" and tamanho_col in df_mapa.columns:
|
| 753 |
+
t_min = df_mapa[tamanho_col].min()
|
| 754 |
+
t_max = df_mapa[tamanho_col].max()
|
| 755 |
+
if t_max > t_min:
|
| 756 |
+
tamanho_func = lambda v, _min=t_min, _max=t_max: raio_min + (v - _min) / (_max - _min) * (raio_max - raio_min)
|
| 757 |
+
else:
|
| 758 |
+
tamanho_func = lambda v: (raio_min + raio_max) / 2
|
| 759 |
+
|
| 760 |
+
# Adiciona pontos
|
| 761 |
+
for idx, row in df_mapa.iterrows():
|
| 762 |
+
# Cor do ponto
|
| 763 |
+
if colormap and cor_col:
|
| 764 |
+
cor = colormap(row[cor_col])
|
| 765 |
+
else:
|
| 766 |
+
cor = COR_PRINCIPAL
|
| 767 |
+
|
| 768 |
+
# Calcula raio
|
| 769 |
+
if tamanho_func:
|
| 770 |
+
raio = tamanho_func(row[tamanho_col])
|
| 771 |
+
else:
|
| 772 |
+
raio = 8
|
| 773 |
+
|
| 774 |
+
# Popup com informações
|
| 775 |
+
itens = []
|
| 776 |
+
for col, val in row.items():
|
| 777 |
+
if str(col).lower() not in ['lat', 'latitude', 'lon', 'longitude']:
|
| 778 |
+
col_norm = str(col).lower()
|
| 779 |
+
if isinstance(val, (int, float, np.floating)):
|
| 780 |
+
if any(k in col_norm for k in ["valor", "preco", "vu", "vunit"]):
|
| 781 |
+
val_fmt = formatar_monetario(val)
|
| 782 |
+
else:
|
| 783 |
+
val_fmt = f"{val:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 784 |
+
else:
|
| 785 |
+
val_fmt = str(val)
|
| 786 |
+
itens.append((col, val_fmt))
|
| 787 |
+
|
| 788 |
+
MAX_ITENS = 8
|
| 789 |
+
colunas_html = []
|
| 790 |
+
for i in range(0, len(itens), MAX_ITENS):
|
| 791 |
+
chunk = itens[i:i+MAX_ITENS]
|
| 792 |
+
trs = "".join([f"<tr style='border-bottom: 1px solid #e9ecef;'><td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500;'>{c}</td><td style='padding:4px 0; text-align:right; color:#495057;'>{v}</td></tr>" for c, v in chunk])
|
| 793 |
+
style = "border-left: 2px solid #dee2e6; padding-left: 20px;" if i > 0 else ""
|
| 794 |
+
colunas_html.append(f"<div style='flex: 0 0 auto; {style}'><table style='border-collapse:collapse; font-size:12px;'>{trs}</table></div>")
|
| 795 |
+
|
| 796 |
+
popup_html = f"""<div style="font-family:'Segoe UI'; border-radius:8px; overflow:hidden;"><div style="background:#6c757d; color:white; padding:10px 15px; font-weight:600;">Dados do Registro</div><div style="padding:12px 15px; background:#f8f9fa;"><div style="display:flex; gap:20px;">{"".join(colunas_html)}</div></div></div>"""
|
| 797 |
+
|
| 798 |
+
# Tooltip (hover): índice + variável selecionada no dropdown (ou dependente como fallback)
|
| 799 |
+
tooltip_col = (
|
| 800 |
+
tamanho_col if (tamanho_col and tamanho_col != "Visualização Padrão"
|
| 801 |
+
and tamanho_col in df_mapa.columns)
|
| 802 |
+
else (col_y if col_y and col_y in df_mapa.columns else None)
|
| 803 |
+
)
|
| 804 |
+
# Usa coluna "index" (original, gerada pelo reset_index) quando disponível
|
| 805 |
+
idx_display = int(row["index"]) if "index" in row.index else idx
|
| 806 |
+
tooltip_html = (
|
| 807 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px;"
|
| 808 |
+
" line-height:1.7; padding:2px 4px;'>"
|
| 809 |
+
f"<b>Índice {idx_display}</b>"
|
| 810 |
+
)
|
| 811 |
+
if tooltip_col and tooltip_col in row.index:
|
| 812 |
+
val_t = row[tooltip_col]
|
| 813 |
+
col_norm = str(tooltip_col).lower()
|
| 814 |
+
if isinstance(val_t, (int, float, np.floating)):
|
| 815 |
+
if any(k in col_norm for k in ["valor", "preco", "vu", "vunit"]):
|
| 816 |
+
val_str = formatar_monetario(val_t)
|
| 817 |
+
else:
|
| 818 |
+
val_str = f"{val_t:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 819 |
+
else:
|
| 820 |
+
val_str = str(val_t)
|
| 821 |
+
tooltip_html += (
|
| 822 |
+
f"<br><span style='color:#555;'>{tooltip_col}:</span>"
|
| 823 |
+
f" <b>{val_str}</b>"
|
| 824 |
+
)
|
| 825 |
+
tooltip_html += "</div>"
|
| 826 |
+
|
| 827 |
+
folium.CircleMarker(
|
| 828 |
+
location=[row[lat_real], row[lon_real]],
|
| 829 |
+
radius=raio,
|
| 830 |
+
popup=folium.Popup(popup_html, max_width=280 * len(colunas_html)),
|
| 831 |
+
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 832 |
+
color='black',
|
| 833 |
+
weight=1,
|
| 834 |
+
fill=True,
|
| 835 |
+
fillColor=cor,
|
| 836 |
+
fillOpacity=0.7
|
| 837 |
+
).add_to(m)
|
| 838 |
+
|
| 839 |
+
# Controles
|
| 840 |
+
folium.LayerControl().add_to(m)
|
| 841 |
+
plugins.Fullscreen().add_to(m)
|
| 842 |
+
plugins.MeasureControl(
|
| 843 |
+
primary_length_unit='meters',
|
| 844 |
+
secondary_length_unit='kilometers',
|
| 845 |
+
primary_area_unit='sqmeters',
|
| 846 |
+
secondary_area_unit='hectares'
|
| 847 |
+
).add_to(m)
|
| 848 |
+
|
| 849 |
+
# Ajusta bounds
|
| 850 |
+
bounds = [
|
| 851 |
+
[df_mapa[lat_real].min(), df_mapa[lon_real].min()],
|
| 852 |
+
[df_mapa[lat_real].max(), df_mapa[lon_real].max()]
|
| 853 |
+
]
|
| 854 |
+
m.fit_bounds(bounds)
|
| 855 |
+
|
| 856 |
+
return m._repr_html_()
|
| 857 |
+
|
| 858 |
+
# ============================================================
|
| 859 |
+
# FUNÇÃO: CARREGAR + VALIDAR MODELO (.dai)
|
| 860 |
+
# ============================================================
|
| 861 |
+
def carregar_modelo_gradio(arquivo):
|
| 862 |
+
if arquivo is None: return None, "Nenhum arquivo enviado."
|
| 863 |
+
try:
|
| 864 |
+
pacote = load(arquivo.name)
|
| 865 |
+
if not isinstance(pacote, dict): return None, "Arquivo inválido."
|
| 866 |
+
# Retrocompatibilidade: converte v1 (flat) para v2 (nested)
|
| 867 |
+
if "versao" not in pacote:
|
| 868 |
+
pacote = _migrar_pacote_v1_para_v2(pacote)
|
| 869 |
+
faltantes = [k for k in CHAVES_ESPERADAS if k not in pacote]
|
| 870 |
+
if faltantes: return None, f"Pacote incompleto. Faltando: {faltantes}"
|
| 871 |
+
return pacote, f"Modelo carregado: {os.path.basename(arquivo.name)}"
|
| 872 |
+
except Exception as e: return None, f"Erro: {e}"
|
| 873 |
+
|
| 874 |
+
# ============================================================
|
| 875 |
+
# FUNÇÃO: DESEMPACOTAR + EXIBIR CONTEÚDO
|
| 876 |
+
# ============================================================
|
| 877 |
+
def exibir_modelo(pacote):
|
| 878 |
+
# Retorna Nones se pacote vazio. Total de outputs = 14 (+ dropdown choices)
|
| 879 |
+
if pacote is None:
|
| 880 |
+
return [None] * 13 + [gr.update(choices=["Visualização Padrão"])]
|
| 881 |
+
|
| 882 |
+
# 1. Dados
|
| 883 |
+
dados = pacote["dados"]["df"].reset_index()
|
| 884 |
+
for col in dados.columns:
|
| 885 |
+
if str(col).lower() in ["lat", "lon"]: dados[col] = dados[col].round(6)
|
| 886 |
+
elif pd.api.types.is_numeric_dtype(dados[col]): dados[col] = dados[col].round(2)
|
| 887 |
+
|
| 888 |
+
# 2. Estatísticas
|
| 889 |
+
estat = pd.DataFrame(pacote["dados"]["estatisticas"])
|
| 890 |
+
if not isinstance(estat.index, pd.RangeIndex):
|
| 891 |
+
estat.insert(0, "Variável", estat.index.astype(str))
|
| 892 |
+
estat = estat.reset_index(drop=True)
|
| 893 |
+
estat = estat.round(2)
|
| 894 |
+
|
| 895 |
+
# 3. Escalas
|
| 896 |
+
escalas_html = formatar_escalas_html(pacote["transformacoes"]["info"])
|
| 897 |
+
|
| 898 |
+
# 4. X e y
|
| 899 |
+
X = pacote["transformacoes"]["X"].reset_index()
|
| 900 |
+
y = pacote["transformacoes"]["y"].reset_index()
|
| 901 |
+
if 'index' in y.columns and 'index' in X.columns: y = y.drop(columns=['index'])
|
| 902 |
+
df_X_y = pd.concat([X, y], axis=1).loc[:, ~pd.concat([X, y], axis=1).columns.duplicated()].round(2)
|
| 903 |
+
|
| 904 |
+
# 5. Resumo
|
| 905 |
+
resumo_html = formatar_resumo_html(reorganizar_modelos_resumos(pacote["modelo"]["diagnosticos"]))
|
| 906 |
+
|
| 907 |
+
# 6. Coeficientes
|
| 908 |
+
tab_coef = pd.DataFrame(pacote["modelo"]["coeficientes"])
|
| 909 |
+
if not isinstance(tab_coef.index, pd.RangeIndex):
|
| 910 |
+
tab_coef.insert(0, "Variável", tab_coef.index.astype(str))
|
| 911 |
+
tab_coef = tab_coef.reset_index(drop=True)
|
| 912 |
+
mask = tab_coef["Variável"].str.lower().isin(["intercept", "const", "(intercept)"])
|
| 913 |
+
if mask.any(): tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
|
| 914 |
+
tab_coef = tab_coef.round(2)
|
| 915 |
+
|
| 916 |
+
# 7. Obs x Calc
|
| 917 |
+
tab_obs_calc = pacote["modelo"]["obs_calc"].reset_index().round(2)
|
| 918 |
+
|
| 919 |
+
# 8. Gráficos (GERAÇÃO DINÂMICA)
|
| 920 |
+
figs_dict = gerar_todos_graficos(pacote)
|
| 921 |
+
|
| 922 |
+
# 9. Mapa
|
| 923 |
+
info_transf = pacote["transformacoes"]["info"]
|
| 924 |
+
nome_y = info_transf[0].split(": ", 1)[0].strip() if info_transf else None
|
| 925 |
+
mapa_html = criar_mapa(dados, col_y=nome_y)
|
| 926 |
+
|
| 927 |
+
# 10. Dropdown de variáveis para o mapa
|
| 928 |
+
colunas_numericas = [col for col in dados.select_dtypes(include=[np.number]).columns
|
| 929 |
+
if str(col).lower() not in ['lat', 'lon', 'latitude', 'longitude', 'index']]
|
| 930 |
+
choices_mapa = ["Visualização Padrão"] + colunas_numericas
|
| 931 |
+
|
| 932 |
+
return (
|
| 933 |
+
dados,
|
| 934 |
+
estat,
|
| 935 |
+
escalas_html,
|
| 936 |
+
df_X_y,
|
| 937 |
+
resumo_html,
|
| 938 |
+
tab_coef,
|
| 939 |
+
tab_obs_calc,
|
| 940 |
+
figs_dict["obs_calc"], # Plot 1
|
| 941 |
+
figs_dict["residuos"], # Plot 2
|
| 942 |
+
figs_dict["hist"], # Plot 3
|
| 943 |
+
figs_dict["cook"], # Plot 4
|
| 944 |
+
figs_dict["corr"], # Plot 5
|
| 945 |
+
mapa_html,
|
| 946 |
+
gr.update(choices=choices_mapa, value="Visualização Padrão") # Dropdown update
|
| 947 |
+
)
|
| 948 |
+
|
| 949 |
+
|
| 950 |
+
def atualizar_mapa_callback(dados, var_mapa, pacote):
|
| 951 |
+
"""Atualiza o mapa quando a variável de dimensionamento é alterada."""
|
| 952 |
+
if dados is None or dados.empty:
|
| 953 |
+
return "<p>Carregue dados para ver o mapa.</p>"
|
| 954 |
+
|
| 955 |
+
tamanho_col = None if var_mapa == "Visualização Padrão" else var_mapa
|
| 956 |
+
nome_y = None
|
| 957 |
+
if pacote:
|
| 958 |
+
info_transf = pacote["transformacoes"]["info"]
|
| 959 |
+
nome_y = info_transf[0].split(": ", 1)[0].strip() if info_transf else None
|
| 960 |
+
return criar_mapa(dados, tamanho_col=tamanho_col, col_y=nome_y)
|
| 961 |
+
|
| 962 |
+
def popular_inputs_avaliacao(pacote):
|
| 963 |
+
"""Popula campos de avaliação a partir do pacote .dai carregado.
|
| 964 |
+
|
| 965 |
+
CONTRACT: Retorna 30 itens (N_ROWS_AVAL + MAX_VARS_AVAL).
|
| 966 |
+
"""
|
| 967 |
+
rows_hidden = [gr.update(visible=False)] * N_ROWS_AVAL
|
| 968 |
+
inputs_hidden = [gr.update(visible=False, value=None, label="")] * MAX_VARS_AVAL
|
| 969 |
+
|
| 970 |
+
if pacote is None:
|
| 971 |
+
return (*rows_hidden, *inputs_hidden)
|
| 972 |
+
|
| 973 |
+
info_transf = pacote["transformacoes"]["info"]
|
| 974 |
+
estatisticas = pacote["dados"]["estatisticas"]
|
| 975 |
+
# Normaliza para string — colunas_x vem de split de strings, mas listas do DAI
|
| 976 |
+
# podem ter inteiros se o CSV original tinha colunas numéricas (ex: anos 2015, 2016...)
|
| 977 |
+
dicotomicas = [str(d) for d in (pacote["transformacoes"].get("dicotomicas", []) or [])]
|
| 978 |
+
codigo_alocado = [str(c) for c in (pacote["transformacoes"].get("codigo_alocado", []) or [])]
|
| 979 |
+
percentuais = [str(p) for p in (pacote["transformacoes"].get("percentuais", []) or [])]
|
| 980 |
+
|
| 981 |
+
colunas_x = []
|
| 982 |
+
for item in info_transf[1:]:
|
| 983 |
+
nome_x, _ = item.split(": ", 1)
|
| 984 |
+
colunas_x.append(nome_x.strip())
|
| 985 |
+
|
| 986 |
+
n_vars = len(colunas_x)
|
| 987 |
+
n_rows_vis = math.ceil(n_vars / N_COLS_AVAL)
|
| 988 |
+
|
| 989 |
+
if estatisticas is not None:
|
| 990 |
+
est = pd.DataFrame(estatisticas)
|
| 991 |
+
if "Variável" in est.columns:
|
| 992 |
+
est_idx = est.set_index("Variável")
|
| 993 |
+
elif not isinstance(est.index, pd.RangeIndex):
|
| 994 |
+
est_idx = est
|
| 995 |
+
else:
|
| 996 |
+
est_idx = pd.DataFrame()
|
| 997 |
+
# Normaliza index para string (mesmo motivo das listas acima)
|
| 998 |
+
if not est_idx.empty:
|
| 999 |
+
est_idx.index = est_idx.index.map(str)
|
| 1000 |
+
else:
|
| 1001 |
+
est_idx = pd.DataFrame()
|
| 1002 |
+
|
| 1003 |
+
rows_updates = [gr.update(visible=(r < n_rows_vis)) for r in range(N_ROWS_AVAL)]
|
| 1004 |
+
|
| 1005 |
+
inputs_updates = []
|
| 1006 |
+
for i in range(MAX_VARS_AVAL):
|
| 1007 |
+
if i < n_vars:
|
| 1008 |
+
col = colunas_x[i]
|
| 1009 |
+
if col in dicotomicas:
|
| 1010 |
+
placeholder = "0 ou 1"
|
| 1011 |
+
elif col in codigo_alocado and col in est_idx.index:
|
| 1012 |
+
min_val = est_idx.loc[col, "Mínimo"]
|
| 1013 |
+
max_val = est_idx.loc[col, "Máximo"]
|
| 1014 |
+
placeholder = f"cód. {int(min_val)} a {int(max_val)}"
|
| 1015 |
+
elif col in percentuais:
|
| 1016 |
+
placeholder = "0 a 1"
|
| 1017 |
+
elif col in est_idx.index:
|
| 1018 |
+
min_val = est_idx.loc[col, "Mínimo"]
|
| 1019 |
+
max_val = est_idx.loc[col, "Máximo"]
|
| 1020 |
+
placeholder = f"{min_val} — {max_val}"
|
| 1021 |
+
else:
|
| 1022 |
+
placeholder = ""
|
| 1023 |
+
inputs_updates.append(gr.update(visible=True, value=None, label=col, placeholder=placeholder, interactive=True))
|
| 1024 |
+
else:
|
| 1025 |
+
inputs_updates.append(gr.update(visible=False, value=None, label="", placeholder=""))
|
| 1026 |
+
|
| 1027 |
+
return (*rows_updates, *inputs_updates)
|
| 1028 |
+
|
| 1029 |
+
|
| 1030 |
+
def calcular_avaliacao_viz(pacote, estado_avaliacoes, indice_base_str, *aval_inputs):
|
| 1031 |
+
"""Calcula avaliação usando o modelo carregado na aba Visualização.
|
| 1032 |
+
|
| 1033 |
+
CONTRACT: Retorna 3 itens (resultado_html, estado_avaliacoes, dropdown_update).
|
| 1034 |
+
"""
|
| 1035 |
+
_err = lambda msg: (msg, estado_avaliacoes or [], gr.update())
|
| 1036 |
+
if pacote is None:
|
| 1037 |
+
return _err("Carregue e exiba um modelo primeiro.")
|
| 1038 |
+
|
| 1039 |
+
info_transf = pacote["transformacoes"]["info"]
|
| 1040 |
+
nome_y, transf_y = info_transf[0].split(": ", 1)
|
| 1041 |
+
transformacao_y = transf_y.strip().replace("(y)", "(x)")
|
| 1042 |
+
|
| 1043 |
+
colunas_x = []
|
| 1044 |
+
transformacoes_x = {}
|
| 1045 |
+
for item in info_transf[1:]:
|
| 1046 |
+
nome_x, transf_x = item.split(": ", 1)
|
| 1047 |
+
nome_x = nome_x.strip()
|
| 1048 |
+
colunas_x.append(nome_x)
|
| 1049 |
+
transformacoes_x[nome_x] = transf_x.strip()
|
| 1050 |
+
|
| 1051 |
+
valores_x = {}
|
| 1052 |
+
for i, col in enumerate(colunas_x):
|
| 1053 |
+
if i >= len(aval_inputs) or aval_inputs[i] is None:
|
| 1054 |
+
return _err("Preencha todos os campos.")
|
| 1055 |
+
valores_x[col] = float(aval_inputs[i])
|
| 1056 |
+
|
| 1057 |
+
# Validar variáveis dicotômicas, código alocado e percentuais ANTES de avaliar
|
| 1058 |
+
dicotomicas = pacote["transformacoes"].get("dicotomicas", [])
|
| 1059 |
+
codigo_alocado = pacote["transformacoes"].get("codigo_alocado", [])
|
| 1060 |
+
percentuais = pacote["transformacoes"].get("percentuais", [])
|
| 1061 |
+
estatisticas_df = pacote["dados"]["estatisticas"]
|
| 1062 |
+
import pandas as pd
|
| 1063 |
+
if isinstance(estatisticas_df, pd.DataFrame):
|
| 1064 |
+
if "Variável" in estatisticas_df.columns:
|
| 1065 |
+
est_idx = estatisticas_df.set_index("Variável")
|
| 1066 |
+
else:
|
| 1067 |
+
est_idx = estatisticas_df
|
| 1068 |
+
else:
|
| 1069 |
+
est_idx = pd.DataFrame()
|
| 1070 |
+
|
| 1071 |
+
for col in colunas_x:
|
| 1072 |
+
val = valores_x[col]
|
| 1073 |
+
if col in (dicotomicas or []):
|
| 1074 |
+
if val not in (0, 0.0, 1, 1.0):
|
| 1075 |
+
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é dicotômica e aceita apenas valores 0 ou 1. "
|
| 1076 |
+
f"Valor informado: {val}</p>")
|
| 1077 |
+
elif col in (codigo_alocado or []) and col in est_idx.index:
|
| 1078 |
+
min_val = float(est_idx.loc[col, "Mínimo"])
|
| 1079 |
+
max_val = float(est_idx.loc[col, "Máximo"])
|
| 1080 |
+
eh_inteiro = (float(val) == int(float(val)))
|
| 1081 |
+
if not eh_inteiro or val < min_val or val > max_val:
|
| 1082 |
+
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é de código alocado/ajustado e aceita apenas "
|
| 1083 |
+
f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
|
| 1084 |
+
elif col in (percentuais or []):
|
| 1085 |
+
if val < 0 or val > 1:
|
| 1086 |
+
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é percentual e aceita apenas "
|
| 1087 |
+
f"valores entre 0 e 1. Valor informado: {val}</p>")
|
| 1088 |
+
|
| 1089 |
+
resultado = avaliar_imovel(
|
| 1090 |
+
modelo_sm=pacote["modelo"]["sm"],
|
| 1091 |
+
valores_x=valores_x,
|
| 1092 |
+
colunas_x=colunas_x,
|
| 1093 |
+
transformacoes_x=transformacoes_x,
|
| 1094 |
+
transformacao_y=transformacao_y,
|
| 1095 |
+
estatisticas_df=estatisticas_df,
|
| 1096 |
+
dicotomicas=dicotomicas,
|
| 1097 |
+
codigo_alocado=codigo_alocado,
|
| 1098 |
+
percentuais=percentuais,
|
| 1099 |
+
)
|
| 1100 |
+
|
| 1101 |
+
if resultado is None:
|
| 1102 |
+
return _err("Erro ao calcular avaliação.")
|
| 1103 |
+
|
| 1104 |
+
nova_lista = list(estado_avaliacoes or []) + [resultado]
|
| 1105 |
+
indice_base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 1106 |
+
html = formatar_avaliacao_html(nova_lista, indice_base=indice_base, elem_id_excluir="excluir-aval-viz")
|
| 1107 |
+
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 1108 |
+
base_val = indice_base_str if indice_base_str else "1"
|
| 1109 |
+
return html, nova_lista, gr.update(choices=choices, value=base_val)
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
def excluir_avaliacao_viz(indice_str, estado_avaliacoes, indice_base_str):
|
| 1113 |
+
"""Exclui uma avaliação (Visualização).
|
| 1114 |
+
|
| 1115 |
+
CONTRACT: Retorna 4 itens (resultado_html, estado_avaliacoes, dropdown_update, trigger_reset).
|
| 1116 |
+
"""
|
| 1117 |
+
if not indice_str or not indice_str.strip() or not estado_avaliacoes:
|
| 1118 |
+
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 1119 |
+
try:
|
| 1120 |
+
idx = int(indice_str.strip()) - 1
|
| 1121 |
+
except ValueError:
|
| 1122 |
+
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 1123 |
+
if idx < 0 or idx >= len(estado_avaliacoes):
|
| 1124 |
+
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 1125 |
+
|
| 1126 |
+
nova_lista = [a for i, a in enumerate(estado_avaliacoes) if i != idx]
|
| 1127 |
+
if not nova_lista:
|
| 1128 |
+
return "", [], gr.update(choices=[], value=None), ""
|
| 1129 |
+
|
| 1130 |
+
base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 1131 |
+
if base >= len(nova_lista):
|
| 1132 |
+
base = len(nova_lista) - 1
|
| 1133 |
+
if base < 0:
|
| 1134 |
+
base = 0
|
| 1135 |
+
|
| 1136 |
+
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 1137 |
+
html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-viz")
|
| 1138 |
+
return html, nova_lista, gr.update(choices=choices, value=str(base + 1)), ""
|
| 1139 |
+
|
| 1140 |
+
|
| 1141 |
+
def atualizar_base_avaliacao_viz(estado_avaliacoes, indice_base_str):
|
| 1142 |
+
"""Re-renderiza HTML quando o dropdown de base muda (Visualização)."""
|
| 1143 |
+
if not estado_avaliacoes:
|
| 1144 |
+
return ""
|
| 1145 |
+
indice = int(indice_base_str) - 1 if indice_base_str else 0
|
| 1146 |
+
return formatar_avaliacao_html(estado_avaliacoes, indice_base=indice, elem_id_excluir="excluir-aval-viz")
|
| 1147 |
+
|
| 1148 |
+
|
| 1149 |
+
def exportar_avaliacoes_excel_viz(estado_avaliacoes):
|
| 1150 |
+
"""Exporta avaliações para Excel (Visualização)."""
|
| 1151 |
+
if not estado_avaliacoes:
|
| 1152 |
+
return gr.update(value=None, visible=False)
|
| 1153 |
+
caminho = exportar_avaliacoes_excel(estado_avaliacoes)
|
| 1154 |
+
if caminho:
|
| 1155 |
+
return gr.update(value=caminho, visible=True)
|
| 1156 |
+
return gr.update(value=None, visible=False)
|
| 1157 |
+
|
| 1158 |
+
|
| 1159 |
+
def _formatar_badge_completo(pacote):
|
| 1160 |
+
"""Retorna HTML do badge combinado: elaborador (esquerda) + variáveis (direita) com flex space-between."""
|
| 1161 |
+
if not pacote:
|
| 1162 |
+
return ""
|
| 1163 |
+
|
| 1164 |
+
# --- Lado direito: lista de variáveis ---
|
| 1165 |
+
variaveis_html = ""
|
| 1166 |
+
info = pacote.get("transformacoes", {}).get("info")
|
| 1167 |
+
if info:
|
| 1168 |
+
y_str = info[0]
|
| 1169 |
+
y_parts = y_str.split(": ", 1)
|
| 1170 |
+
y_nome = y_parts[0].strip()
|
| 1171 |
+
y_transf = y_parts[1].strip() if len(y_parts) > 1 else ""
|
| 1172 |
+
y_transf_display = "" if y_transf in ("(y)", "(x)", "y", "x", "") else y_transf
|
| 1173 |
+
y_badge = (
|
| 1174 |
+
"<span style='background:#cce5ff;color:#004085;border-radius:4px;"
|
| 1175 |
+
f"padding:3px 10px;font-size:1em;font-weight:600;'>{y_nome}"
|
| 1176 |
+
+ (f" <span style='font-weight:400;font-size:0.85em;'>{y_transf_display}</span>"
|
| 1177 |
+
if y_transf_display else "")
|
| 1178 |
+
+ "</span>"
|
| 1179 |
+
)
|
| 1180 |
+
x_badges = []
|
| 1181 |
+
for item in info[1:]:
|
| 1182 |
+
parts = item.split(": ", 1)
|
| 1183 |
+
nome = parts[0].strip()
|
| 1184 |
+
transf = parts[1].strip() if len(parts) > 1 else ""
|
| 1185 |
+
transf_display = "" if transf in ("(x)", "(y)", "x", "y", "") else transf
|
| 1186 |
+
badge = (
|
| 1187 |
+
"<span style='background:#ced4da;color:#343a40;border-radius:4px;"
|
| 1188 |
+
f"padding:3px 8px;font-size:1em;display:inline-block;margin:2px 3px 2px 0;'>{nome}"
|
| 1189 |
+
+ (f" <span style='color:#6c757d;font-size:1em;'>{transf_display}</span>"
|
| 1190 |
+
if transf_display else "")
|
| 1191 |
+
+ "</span>"
|
| 1192 |
+
)
|
| 1193 |
+
x_badges.append(badge)
|
| 1194 |
+
variaveis_html = (
|
| 1195 |
+
"<div style='font-size:0.9em;line-height:2;'>"
|
| 1196 |
+
"<div><span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;'>Variável Dependente:</span>"
|
| 1197 |
+
+ y_badge + "</div>"
|
| 1198 |
+
"<div style='margin-top:4px;'>"
|
| 1199 |
+
"<span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;'>Variáveis Independentes:</span>"
|
| 1200 |
+
+ "".join(x_badges) + "</div>"
|
| 1201 |
+
"</div>"
|
| 1202 |
+
)
|
| 1203 |
+
|
| 1204 |
+
# --- Lado esquerdo: elaborador ---
|
| 1205 |
+
elaborador = pacote.get("elaborador")
|
| 1206 |
+
nome = elaborador.get("nome_completo", "") if elaborador else ""
|
| 1207 |
+
if not nome:
|
| 1208 |
+
# Sem elaborador: só exibe variáveis (sem flex desnecessário)
|
| 1209 |
+
if not variaveis_html:
|
| 1210 |
+
return ""
|
| 1211 |
+
return (
|
| 1212 |
+
'<div style="background:#e9ecef; border-left:5px solid #6c757d; border-radius:6px; '
|
| 1213 |
+
'padding:14px 18px; margin-top:8px;">'
|
| 1214 |
+
+ variaveis_html
|
| 1215 |
+
+ '</div>'
|
| 1216 |
+
)
|
| 1217 |
+
|
| 1218 |
+
cargo = elaborador.get("cargo", "")
|
| 1219 |
+
conselho = elaborador.get("conselho", "")
|
| 1220 |
+
num = elaborador.get("numero_conselho", "")
|
| 1221 |
+
estado = elaborador.get("estado_conselho", "")
|
| 1222 |
+
matricula = elaborador.get("matricula_sem_digito", "")
|
| 1223 |
+
lotacao = elaborador.get("lotacao", "")
|
| 1224 |
+
linha2 = f"{cargo} · {conselho}/{estado} {num}".strip(" ·") if cargo else ""
|
| 1225 |
+
linha3 = f"Matrícula: {matricula} · {lotacao}".strip(" ·") if matricula else ""
|
| 1226 |
+
|
| 1227 |
+
elab_html = (
|
| 1228 |
+
'<div style="line-height:1.8;">'
|
| 1229 |
+
f'<span style="display:block; font-size:1.05em; font-weight:600; color:#212529; margin-bottom:4px;">{nome}</span>'
|
| 1230 |
+
+ (f'<span style="display:block; font-size:0.95em;">{linha2}</span>' if linha2 else '')
|
| 1231 |
+
+ (f'<span style="display:block; font-size:0.9em; color:#6c757d;">{linha3}</span>' if linha3 else '')
|
| 1232 |
+
+ '</div>'
|
| 1233 |
+
)
|
| 1234 |
+
|
| 1235 |
+
return (
|
| 1236 |
+
'<div style="background:#e9ecef; border-left:5px solid #6c757d; border-radius:6px; '
|
| 1237 |
+
'padding:14px 18px; color:#495057; margin-top:8px; display:flex; '
|
| 1238 |
+
'justify-content:space-between; align-items:flex-start; gap:24px;">'
|
| 1239 |
+
+ elab_html
|
| 1240 |
+
+ (f'<div>{variaveis_html}</div>' if variaveis_html else '')
|
| 1241 |
+
+ '</div>'
|
| 1242 |
+
)
|
| 1243 |
+
|
| 1244 |
+
|
| 1245 |
+
def limpar_tudo():
|
| 1246 |
+
return (
|
| 1247 |
+
None, "", None, None, None, "", None, "", None, None,
|
| 1248 |
+
None, None, None, None, None, # 5 gráficos nulos
|
| 1249 |
+
"", None,
|
| 1250 |
+
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"), # Dropdown reset
|
| 1251 |
+
# Reset avaliação
|
| 1252 |
+
*[gr.update(visible=False) for _ in range(N_ROWS_AVAL)],
|
| 1253 |
+
*[gr.update(visible=False, value=None, label="") for _ in range(MAX_VARS_AVAL)],
|
| 1254 |
+
"", # resultado_aval_html
|
| 1255 |
+
[], # estado_avaliacoes
|
| 1256 |
+
gr.update(choices=[], value=None), # dropdown_base_aval
|
| 1257 |
+
"", # excluir_aval_trigger_viz
|
| 1258 |
+
gr.update(value=None, visible=False), # download_aval_file
|
| 1259 |
+
"", # elaborador_badge
|
| 1260 |
+
)
|
| 1261 |
+
|
| 1262 |
+
# ============================================================
|
| 1263 |
+
# INTERFACE GRADIO
|
| 1264 |
+
# ============================================================
|
| 1265 |
+
|
| 1266 |
+
description = f"""
|
| 1267 |
+
# <p style="text-align: center;">MODELOS ESTATÍSTICOS</p>
|
| 1268 |
+
<p style="text-align: center;">Divisão de Avaliação de Imóveis</p>
|
| 1269 |
+
<hr style="color: #333; background-color: #333; height: 1px; border: none;">
|
| 1270 |
+
"""
|
| 1271 |
+
|
| 1272 |
+
def criar_aba():
|
| 1273 |
+
"""Cria conteúdo da aba de visualização (sem wrapper gr.Blocks)."""
|
| 1274 |
+
# --------------------------------------------------------
|
| 1275 |
+
# ESTADO
|
| 1276 |
+
# --------------------------------------------------------
|
| 1277 |
+
estado_pacote = gr.State(None)
|
| 1278 |
+
estado_dados = gr.State(None) # Armazena os dados para atualizar o mapa
|
| 1279 |
+
|
| 1280 |
+
# --------------------------------------------------------
|
| 1281 |
+
# CONTROLES (TOPO)
|
| 1282 |
+
# --------------------------------------------------------
|
| 1283 |
+
with gr.Group(elem_classes="upload-area"):
|
| 1284 |
+
upload = gr.File(label="Enviar modelo salvo (.dai)", file_types=[".dai"], scale=1)
|
| 1285 |
+
with gr.Row(equal_height=True):
|
| 1286 |
+
status = gr.Textbox(show_label=False, interactive=False, scale=4, lines=1)
|
| 1287 |
+
elaborador_badge = gr.HTML("")
|
| 1288 |
+
with gr.Row(equal_height=True):
|
| 1289 |
+
btn_exibir = gr.Button("Exibir modelo", scale=2, variant="primary")
|
| 1290 |
+
btn_limpar = gr.Button("Limpar tudo", scale=1, variant="secondary")
|
| 1291 |
+
|
| 1292 |
+
# --------------------------------------------------------
|
| 1293 |
+
# MAPA (RETRÁTIL VIA ACCORDION)
|
| 1294 |
+
# --------------------------------------------------------
|
| 1295 |
+
with gr.Accordion("Mapa de Distribuição dos Dados", open=True, elem_classes="map-accordion"):
|
| 1296 |
+
dropdown_mapa_var = gr.Dropdown(
|
| 1297 |
+
label="Variável para dimensionar pontos no mapa",
|
| 1298 |
+
choices=["Visualização Padrão"],
|
| 1299 |
+
value="Visualização Padrão",
|
| 1300 |
+
interactive=True,
|
| 1301 |
+
allow_custom_value=False
|
| 1302 |
+
)
|
| 1303 |
+
out_mapa = gr.HTML(label="", elem_id="map-frame")
|
| 1304 |
+
|
| 1305 |
+
# --------------------------------------------------------
|
| 1306 |
+
# CONTEÚDO (LARGURA TOTAL)
|
| 1307 |
+
# --------------------------------------------------------
|
| 1308 |
+
with gr.Column(elem_classes="content-panel"):
|
| 1309 |
+
with gr.Tabs(elem_classes="tabs-container"):
|
| 1310 |
+
with gr.Tab("Dados"):
|
| 1311 |
+
gr.HTML(criar_titulo_secao_html("Dados Utilizados"))
|
| 1312 |
+
out_dados = gr.Dataframe(show_label=False, max_height=300)
|
| 1313 |
+
gr.HTML(criar_titulo_secao_html("Estatísticas"))
|
| 1314 |
+
out_estat = gr.Dataframe(show_label=False, max_height=300)
|
| 1315 |
+
|
| 1316 |
+
with gr.Tab("Transformações"):
|
| 1317 |
+
out_escalas = gr.HTML()
|
| 1318 |
+
gr.HTML(criar_titulo_secao_html("X e y Transformados"))
|
| 1319 |
+
out_df_xy = gr.Dataframe(show_label=False, max_height=400)
|
| 1320 |
+
|
| 1321 |
+
with gr.Tab("Resumo"):
|
| 1322 |
+
out_resumo = gr.HTML()
|
| 1323 |
+
|
| 1324 |
+
with gr.Tab("Coeficientes"):
|
| 1325 |
+
gr.HTML(criar_titulo_secao_html("Tabela de Coeficientes"))
|
| 1326 |
+
out_coef = gr.Dataframe(show_label=False, max_height=700)
|
| 1327 |
+
|
| 1328 |
+
with gr.Tab("Obs x Calc"):
|
| 1329 |
+
gr.HTML(criar_titulo_secao_html("Tabela Obs x Calc"))
|
| 1330 |
+
out_obs = gr.Dataframe(show_label=False, max_height=600)
|
| 1331 |
+
|
| 1332 |
+
with gr.Tab("Gráficos") as tab_graficos:
|
| 1333 |
+
with gr.Group():
|
| 1334 |
+
# Linha 1 — dois gráficos, 50% / 50%
|
| 1335 |
+
with gr.Row():
|
| 1336 |
+
out_plot_obs = gr.Plot(label="Obs vs Calc")
|
| 1337 |
+
out_plot_res = gr.Plot(label="Resíduos vs Ajustados")
|
| 1338 |
+
|
| 1339 |
+
# Linha 2 — dois gráficos, 50% / 50%
|
| 1340 |
+
with gr.Row():
|
| 1341 |
+
out_plot_hist = gr.Plot(label="Histograma Resíduos")
|
| 1342 |
+
out_plot_cook = gr.Plot(label="Distância de Cook")
|
| 1343 |
+
|
| 1344 |
+
# Linha 3 — gráfico sozinho, largura máxima
|
| 1345 |
+
with gr.Row():
|
| 1346 |
+
out_plot_corr = gr.Plot(label="Correlação")
|
| 1347 |
+
|
| 1348 |
+
with gr.Tab("Avaliação"):
|
| 1349 |
+
gr.HTML(criar_titulo_secao_html("Avaliação Individual"))
|
| 1350 |
+
aval_rows = []
|
| 1351 |
+
aval_inputs = []
|
| 1352 |
+
with gr.Column(elem_classes="aval-all-cards"):
|
| 1353 |
+
for _i_row in range(N_ROWS_AVAL):
|
| 1354 |
+
with gr.Row(visible=False, elem_classes="aval-cards-row") as _aval_row:
|
| 1355 |
+
for _j_col in range(N_COLS_AVAL):
|
| 1356 |
+
_inp = gr.Number(label="", visible=False, interactive=True, elem_classes="aval-card")
|
| 1357 |
+
aval_inputs.append(_inp)
|
| 1358 |
+
aval_rows.append(_aval_row)
|
| 1359 |
+
with gr.Row():
|
| 1360 |
+
btn_calcular_aval = gr.Button("Calcular", variant="primary", scale=2)
|
| 1361 |
+
btn_limpar_aval = gr.Button("Limpar", variant="secondary", scale=1)
|
| 1362 |
+
dropdown_base_aval = gr.Dropdown(
|
| 1363 |
+
label="Base p/ comparação",
|
| 1364 |
+
choices=[],
|
| 1365 |
+
value=None,
|
| 1366 |
+
interactive=True,
|
| 1367 |
+
scale=1,
|
| 1368 |
+
)
|
| 1369 |
+
resultado_aval_html = gr.HTML("")
|
| 1370 |
+
excluir_aval_trigger_viz = gr.Textbox(
|
| 1371 |
+
label="", elem_id="excluir-aval-viz", container=False,
|
| 1372 |
+
elem_classes="trigger-hidden"
|
| 1373 |
+
)
|
| 1374 |
+
estado_avaliacoes = gr.State([])
|
| 1375 |
+
with gr.Row():
|
| 1376 |
+
btn_exportar_aval = gr.Button("Salvar Avaliações em Excel", variant="secondary")
|
| 1377 |
+
download_aval_file = gr.File(label="", visible=False)
|
| 1378 |
+
|
| 1379 |
+
with gr.Tab("Avaliação em Massa"):
|
| 1380 |
+
gr.HTML(criar_titulo_secao_html("Avaliação em Lote"))
|
| 1381 |
+
gr.HTML("""<div class="placeholder-alert"><p>Módulo em desenvolvimento</p></div>""")
|
| 1382 |
+
|
| 1383 |
+
# --------------------------------------------------------
|
| 1384 |
+
# EVENTOS
|
| 1385 |
+
# --------------------------------------------------------
|
| 1386 |
+
upload.upload(
|
| 1387 |
+
carregar_modelo_gradio, inputs=upload, outputs=[estado_pacote, status]
|
| 1388 |
+
).then(
|
| 1389 |
+
_formatar_badge_completo, inputs=estado_pacote, outputs=elaborador_badge
|
| 1390 |
+
)
|
| 1391 |
+
|
| 1392 |
+
btn_exibir.click(
|
| 1393 |
+
exibir_modelo,
|
| 1394 |
+
inputs=estado_pacote,
|
| 1395 |
+
outputs=[
|
| 1396 |
+
out_dados, out_estat, out_escalas, out_df_xy, out_resumo, out_coef, out_obs,
|
| 1397 |
+
out_plot_obs, out_plot_res, out_plot_hist, out_plot_cook, out_plot_corr,
|
| 1398 |
+
out_mapa, dropdown_mapa_var
|
| 1399 |
+
],
|
| 1400 |
+
).then(
|
| 1401 |
+
popular_inputs_avaliacao,
|
| 1402 |
+
inputs=estado_pacote,
|
| 1403 |
+
outputs=aval_rows + aval_inputs
|
| 1404 |
+
).then(
|
| 1405 |
+
lambda x: x, # Copia out_dados para estado_dados
|
| 1406 |
+
inputs=out_dados,
|
| 1407 |
+
outputs=estado_dados
|
| 1408 |
+
)
|
| 1409 |
+
|
| 1410 |
+
# Atualiza mapa quando dropdown muda
|
| 1411 |
+
dropdown_mapa_var.change(
|
| 1412 |
+
atualizar_mapa_callback,
|
| 1413 |
+
inputs=[estado_dados, dropdown_mapa_var, estado_pacote],
|
| 1414 |
+
outputs=out_mapa
|
| 1415 |
+
)
|
| 1416 |
+
|
| 1417 |
+
btn_limpar.click(
|
| 1418 |
+
limpar_tudo,
|
| 1419 |
+
outputs=[
|
| 1420 |
+
estado_pacote, status, upload,
|
| 1421 |
+
out_dados, out_estat, out_escalas, out_df_xy, out_resumo, out_coef, out_obs,
|
| 1422 |
+
out_plot_obs, out_plot_res, out_plot_hist, out_plot_cook, out_plot_corr,
|
| 1423 |
+
out_mapa, estado_dados,
|
| 1424 |
+
dropdown_mapa_var,
|
| 1425 |
+
] + aval_rows + aval_inputs + [resultado_aval_html, estado_avaliacoes,
|
| 1426 |
+
dropdown_base_aval, excluir_aval_trigger_viz, download_aval_file,
|
| 1427 |
+
elaborador_badge]
|
| 1428 |
+
)
|
| 1429 |
+
|
| 1430 |
+
# Avaliação individual
|
| 1431 |
+
btn_calcular_aval.click(
|
| 1432 |
+
calcular_avaliacao_viz,
|
| 1433 |
+
inputs=[estado_pacote, estado_avaliacoes, dropdown_base_aval] + aval_inputs,
|
| 1434 |
+
outputs=[resultado_aval_html, estado_avaliacoes, dropdown_base_aval]
|
| 1435 |
+
)
|
| 1436 |
+
|
| 1437 |
+
btn_limpar_aval.click(
|
| 1438 |
+
lambda: ("", [], gr.update(choices=[], value=None)),
|
| 1439 |
+
outputs=[resultado_aval_html, estado_avaliacoes, dropdown_base_aval]
|
| 1440 |
+
)
|
| 1441 |
+
|
| 1442 |
+
excluir_aval_trigger_viz.change(
|
| 1443 |
+
excluir_avaliacao_viz,
|
| 1444 |
+
inputs=[excluir_aval_trigger_viz, estado_avaliacoes, dropdown_base_aval],
|
| 1445 |
+
outputs=[resultado_aval_html, estado_avaliacoes, dropdown_base_aval, excluir_aval_trigger_viz]
|
| 1446 |
+
)
|
| 1447 |
+
|
| 1448 |
+
dropdown_base_aval.change(
|
| 1449 |
+
atualizar_base_avaliacao_viz,
|
| 1450 |
+
inputs=[estado_avaliacoes, dropdown_base_aval],
|
| 1451 |
+
outputs=[resultado_aval_html]
|
| 1452 |
+
)
|
| 1453 |
+
|
| 1454 |
+
btn_exportar_aval.click(
|
| 1455 |
+
exportar_avaliacoes_excel_viz,
|
| 1456 |
+
inputs=[estado_avaliacoes],
|
| 1457 |
+
outputs=[download_aval_file]
|
| 1458 |
+
)
|
| 1459 |
+
|
| 1460 |
+
# Força Plotly a recalcular tamanho quando a tab Gráficos é selecionada
|
| 1461 |
+
tab_graficos.select(
|
| 1462 |
+
fn=None,
|
| 1463 |
+
inputs=None,
|
| 1464 |
+
outputs=None,
|
| 1465 |
+
js="() => { setTimeout(() => window.dispatchEvent(new Event('resize')), 100) }"
|
| 1466 |
+
)
|
| 1467 |
+
|
| 1468 |
+
|
| 1469 |
+
# ============================================================
|
| 1470 |
+
# EXECUÇÃO
|
| 1471 |
+
# ============================================================
|
| 1472 |
+
if __name__ == "__main__":
|
| 1473 |
+
custom_css = carregar_css()
|
| 1474 |
+
with gr.Blocks(css=custom_css) as app:
|
| 1475 |
+
gr.Markdown(description)
|
| 1476 |
+
criar_aba()
|
| 1477 |
+
app.launch()
|
backend/app/main.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from fastapi import FastAPI
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from fastapi.responses import FileResponse
|
| 8 |
+
from fastapi.staticfiles import StaticFiles
|
| 9 |
+
|
| 10 |
+
from app.api import elaboracao, health, session, visualizacao
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
app = FastAPI(
|
| 14 |
+
title="MESA Frame API",
|
| 15 |
+
version="1.0.0",
|
| 16 |
+
description="Backend FastAPI para o app MESA (Elaboracao + Visualizacao)",
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
app.add_middleware(
|
| 20 |
+
CORSMiddleware,
|
| 21 |
+
allow_origins=["*"],
|
| 22 |
+
allow_credentials=True,
|
| 23 |
+
allow_methods=["*"],
|
| 24 |
+
allow_headers=["*"],
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
app.include_router(health.router)
|
| 28 |
+
app.include_router(session.router)
|
| 29 |
+
app.include_router(elaboracao.router)
|
| 30 |
+
app.include_router(visualizacao.router)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _mount_frontend_if_exists() -> None:
|
| 34 |
+
frontend_dist = Path(__file__).resolve().parents[2] / "frontend" / "dist"
|
| 35 |
+
index_file = frontend_dist / "index.html"
|
| 36 |
+
if not index_file.exists():
|
| 37 |
+
return
|
| 38 |
+
|
| 39 |
+
app.mount("/assets", StaticFiles(directory=frontend_dist / "assets"), name="assets")
|
| 40 |
+
|
| 41 |
+
@app.get("/{full_path:path}", include_in_schema=False)
|
| 42 |
+
def serve_spa(full_path: str) -> FileResponse:
|
| 43 |
+
return FileResponse(index_file)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
_mount_frontend_if_exists()
|
backend/app/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Models package
|
backend/app/models/session.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class SessionState:
|
| 12 |
+
session_id: str
|
| 13 |
+
workdir: Path
|
| 14 |
+
|
| 15 |
+
uploaded_file_path: str | None = None
|
| 16 |
+
uploaded_filename: str | None = None
|
| 17 |
+
available_sheets: list[str] = field(default_factory=list)
|
| 18 |
+
|
| 19 |
+
df_original: pd.DataFrame | None = None
|
| 20 |
+
df_filtrado: pd.DataFrame | None = None
|
| 21 |
+
|
| 22 |
+
coluna_y: str | None = None
|
| 23 |
+
colunas_x: list[str] = field(default_factory=list)
|
| 24 |
+
dicotomicas: list[str] = field(default_factory=list)
|
| 25 |
+
codigo_alocado: list[str] = field(default_factory=list)
|
| 26 |
+
percentuais: list[str] = field(default_factory=list)
|
| 27 |
+
|
| 28 |
+
transformacao_y: str = "(x)"
|
| 29 |
+
transformacoes_x: dict[str, str] = field(default_factory=dict)
|
| 30 |
+
|
| 31 |
+
resultados_busca: list[dict[str, Any]] = field(default_factory=list)
|
| 32 |
+
resultado_modelo: dict[str, Any] | None = None
|
| 33 |
+
|
| 34 |
+
tabela_estatisticas: pd.DataFrame | None = None
|
| 35 |
+
tabela_metricas_estado: pd.DataFrame | None = None
|
| 36 |
+
|
| 37 |
+
outliers_anteriores: list[int] = field(default_factory=list)
|
| 38 |
+
iteracao: int = 1
|
| 39 |
+
|
| 40 |
+
avaliacoes_elaboracao: list[dict[str, Any]] = field(default_factory=list)
|
| 41 |
+
|
| 42 |
+
geo_falhas_df: pd.DataFrame | None = None
|
| 43 |
+
geo_col_cdlog: str | None = None
|
| 44 |
+
geo_col_num: str | None = None
|
| 45 |
+
|
| 46 |
+
pacote_visualizacao: dict[str, Any] | None = None
|
| 47 |
+
dados_visualizacao: pd.DataFrame | None = None
|
| 48 |
+
avaliacoes_visualizacao: list[dict[str, Any]] = field(default_factory=list)
|
| 49 |
+
|
| 50 |
+
elaborador: dict[str, Any] | None = None
|
| 51 |
+
|
| 52 |
+
def reset_modelo(self) -> None:
|
| 53 |
+
self.resultados_busca = []
|
| 54 |
+
self.resultado_modelo = None
|
| 55 |
+
self.tabela_estatisticas = None
|
| 56 |
+
self.tabela_metricas_estado = None
|
| 57 |
+
self.avaliacoes_elaboracao = []
|
| 58 |
+
self.transformacao_y = "(x)"
|
| 59 |
+
self.transformacoes_x = {}
|
| 60 |
+
|
| 61 |
+
def reset_visualizacao(self) -> None:
|
| 62 |
+
self.pacote_visualizacao = None
|
| 63 |
+
self.dados_visualizacao = None
|
| 64 |
+
self.avaliacoes_visualizacao = []
|
backend/app/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Service layer package
|
backend/app/services/elaboracao_service.py
ADDED
|
@@ -0,0 +1,1124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from dataclasses import asdict
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from fastapi import HTTPException
|
| 12 |
+
|
| 13 |
+
from app.core.elaboracao import charts, geocodificacao
|
| 14 |
+
from app.core.elaboracao.core import (
|
| 15 |
+
TRANSFORMACOES,
|
| 16 |
+
ajustar_modelo,
|
| 17 |
+
buscar_melhores_transformacoes,
|
| 18 |
+
calcular_estatisticas_variaveis,
|
| 19 |
+
carregar_arquivo,
|
| 20 |
+
carregar_dai,
|
| 21 |
+
detectar_abas_excel,
|
| 22 |
+
detectar_codigo_alocado,
|
| 23 |
+
detectar_dicotomicas,
|
| 24 |
+
detectar_percentuais,
|
| 25 |
+
exportar_avaliacoes_excel,
|
| 26 |
+
exportar_base_csv,
|
| 27 |
+
exportar_modelo_dai,
|
| 28 |
+
identificar_coluna_y_padrao,
|
| 29 |
+
obter_colunas_numericas,
|
| 30 |
+
avaliar_imovel,
|
| 31 |
+
testar_micronumerosidade,
|
| 32 |
+
verificar_multicolinearidade,
|
| 33 |
+
)
|
| 34 |
+
from app.core.elaboracao.formatadores import (
|
| 35 |
+
formatar_avaliacao_html,
|
| 36 |
+
formatar_aviso_multicolinearidade,
|
| 37 |
+
formatar_busca_html,
|
| 38 |
+
formatar_diagnosticos_html,
|
| 39 |
+
formatar_micronumerosidade_html,
|
| 40 |
+
formatar_outliers_anteriores_html,
|
| 41 |
+
)
|
| 42 |
+
from app.models.session import SessionState
|
| 43 |
+
from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
|
| 44 |
+
|
| 45 |
+
_AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboracao" / "avaliadores.json"
|
| 46 |
+
_AVALIADORES_CACHE: list[dict[str, Any]] | None = None
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def list_avaliadores() -> list[dict[str, Any]]:
|
| 50 |
+
global _AVALIADORES_CACHE
|
| 51 |
+
if _AVALIADORES_CACHE is not None:
|
| 52 |
+
return _AVALIADORES_CACHE
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
raw = json.loads(_AVALIADORES_PATH.read_text(encoding="utf-8"))
|
| 56 |
+
avaliadores = raw.get("avaliadores", [])
|
| 57 |
+
if isinstance(avaliadores, list):
|
| 58 |
+
_AVALIADORES_CACHE = [sanitize_value(item) for item in avaliadores if isinstance(item, dict)]
|
| 59 |
+
else:
|
| 60 |
+
_AVALIADORES_CACHE = []
|
| 61 |
+
except Exception:
|
| 62 |
+
_AVALIADORES_CACHE = []
|
| 63 |
+
|
| 64 |
+
return _AVALIADORES_CACHE
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _clean_int_list(values: list[Any] | None) -> list[int]:
|
| 68 |
+
if not values:
|
| 69 |
+
return []
|
| 70 |
+
out: list[int] = []
|
| 71 |
+
for value in values:
|
| 72 |
+
try:
|
| 73 |
+
out.append(int(value))
|
| 74 |
+
except Exception:
|
| 75 |
+
continue
|
| 76 |
+
return sorted(set(out))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _parse_indices_text(text: str | None) -> list[int]:
|
| 80 |
+
if not text:
|
| 81 |
+
return []
|
| 82 |
+
items = [piece.strip() for piece in text.split(",") if piece.strip()]
|
| 83 |
+
out: list[int] = []
|
| 84 |
+
for item in items:
|
| 85 |
+
try:
|
| 86 |
+
out.append(int(item))
|
| 87 |
+
except Exception:
|
| 88 |
+
continue
|
| 89 |
+
return sorted(set(out))
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _selection_context(session: SessionState) -> dict[str, Any]:
|
| 93 |
+
return {
|
| 94 |
+
"coluna_y": session.coluna_y,
|
| 95 |
+
"colunas_x": list(session.colunas_x),
|
| 96 |
+
"dicotomicas": list(session.dicotomicas),
|
| 97 |
+
"codigo_alocado": list(session.codigo_alocado),
|
| 98 |
+
"percentuais": list(session.percentuais),
|
| 99 |
+
"outliers_anteriores": list(session.outliers_anteriores),
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def classificar_tipos_variaveis_x(session: SessionState, colunas_x: list[str] | None) -> dict[str, Any]:
|
| 104 |
+
df = session.df_original if session.df_original is not None else session.df_filtrado
|
| 105 |
+
if df is None:
|
| 106 |
+
raise HTTPException(status_code=400, detail="Carregue uma base antes de classificar as variaveis X")
|
| 107 |
+
|
| 108 |
+
coluna_y = str(session.coluna_y) if session.coluna_y else None
|
| 109 |
+
colunas_validas: list[str] = []
|
| 110 |
+
for col in colunas_x or []:
|
| 111 |
+
nome = str(col)
|
| 112 |
+
if nome not in df.columns:
|
| 113 |
+
continue
|
| 114 |
+
if coluna_y and nome == coluna_y:
|
| 115 |
+
continue
|
| 116 |
+
colunas_validas.append(nome)
|
| 117 |
+
|
| 118 |
+
if not colunas_validas:
|
| 119 |
+
session.dicotomicas = []
|
| 120 |
+
session.codigo_alocado = []
|
| 121 |
+
session.percentuais = []
|
| 122 |
+
return {
|
| 123 |
+
"dicotomicas": [],
|
| 124 |
+
"codigo_alocado": [],
|
| 125 |
+
"percentuais": [],
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
dicotomicas = detectar_dicotomicas(df, colunas_validas)
|
| 129 |
+
codigo_alocado = detectar_codigo_alocado(df, colunas_validas)
|
| 130 |
+
percentuais = detectar_percentuais(df, colunas_validas)
|
| 131 |
+
|
| 132 |
+
session.dicotomicas = list(dicotomicas)
|
| 133 |
+
session.codigo_alocado = list(codigo_alocado)
|
| 134 |
+
session.percentuais = list(percentuais)
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"dicotomicas": sanitize_value(dicotomicas),
|
| 138 |
+
"codigo_alocado": sanitize_value(codigo_alocado),
|
| 139 |
+
"percentuais": sanitize_value(percentuais),
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _build_coords_payload(df: pd.DataFrame, tem_coords: bool) -> dict[str, Any]:
|
| 144 |
+
todas_colunas = [str(c) for c in df.columns]
|
| 145 |
+
if tem_coords:
|
| 146 |
+
return {
|
| 147 |
+
"tem_coords": True,
|
| 148 |
+
"aviso_html": "",
|
| 149 |
+
"colunas_disponiveis": [],
|
| 150 |
+
"cdlog_auto": None,
|
| 151 |
+
"num_auto": None,
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
cdlog_auto, num_auto = geocodificacao.auto_detectar_colunas_geo(df)
|
| 155 |
+
aviso_html = (
|
| 156 |
+
'<div style="background:#f8d7da;border:1px solid #f5c2c7;border-radius:8px;'
|
| 157 |
+
f'padding:10px 14px;margin-bottom:8px"><strong>Colunas lat/lon nao encontradas</strong> — {len(df)} registro(s).'
|
| 158 |
+
"</div>"
|
| 159 |
+
)
|
| 160 |
+
return {
|
| 161 |
+
"tem_coords": False,
|
| 162 |
+
"aviso_html": aviso_html,
|
| 163 |
+
"colunas_disponiveis": todas_colunas,
|
| 164 |
+
"cdlog_auto": cdlog_auto,
|
| 165 |
+
"num_auto": num_auto,
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _set_dataframe_base(session: SessionState, df: pd.DataFrame, clear_models: bool = True) -> dict[str, Any]:
|
| 170 |
+
tem_coords, col_lat, col_lon = geocodificacao.verificar_coords(df)
|
| 171 |
+
if tem_coords and col_lat and col_lon:
|
| 172 |
+
df = geocodificacao.padronizar_coords(df, col_lat, col_lon)
|
| 173 |
+
|
| 174 |
+
session.df_original = df.copy()
|
| 175 |
+
session.df_filtrado = df.copy()
|
| 176 |
+
session.geo_falhas_df = None
|
| 177 |
+
session.geo_col_cdlog = None
|
| 178 |
+
session.geo_col_num = None
|
| 179 |
+
|
| 180 |
+
if clear_models:
|
| 181 |
+
session.reset_modelo()
|
| 182 |
+
session.coluna_y = None
|
| 183 |
+
session.colunas_x = []
|
| 184 |
+
session.dicotomicas = []
|
| 185 |
+
session.codigo_alocado = []
|
| 186 |
+
session.percentuais = []
|
| 187 |
+
session.outliers_anteriores = []
|
| 188 |
+
session.iteracao = 1
|
| 189 |
+
|
| 190 |
+
colunas_numericas = [str(c) for c in obter_colunas_numericas(df)]
|
| 191 |
+
coluna_y_padrao = identificar_coluna_y_padrao(df)
|
| 192 |
+
mapa_html = charts.criar_mapa(df)
|
| 193 |
+
|
| 194 |
+
return {
|
| 195 |
+
"dados": dataframe_to_payload(df, decimals=4),
|
| 196 |
+
"mapa_html": mapa_html,
|
| 197 |
+
"colunas_numericas": colunas_numericas,
|
| 198 |
+
"coluna_y_padrao": coluna_y_padrao,
|
| 199 |
+
"coords": _build_coords_payload(df, tem_coords),
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def save_uploaded_file(session: SessionState, filename: str, content: bytes) -> str:
|
| 204 |
+
safe_name = os.path.basename(filename)
|
| 205 |
+
destino = session.workdir / safe_name
|
| 206 |
+
destino.write_bytes(content)
|
| 207 |
+
session.uploaded_file_path = str(destino)
|
| 208 |
+
session.uploaded_filename = safe_name
|
| 209 |
+
return str(destino)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def load_uploaded_file(session: SessionState, selected_sheet: str | None = None) -> dict[str, Any]:
|
| 213 |
+
if not session.uploaded_file_path:
|
| 214 |
+
raise HTTPException(status_code=400, detail="Nenhum arquivo enviado")
|
| 215 |
+
|
| 216 |
+
caminho = session.uploaded_file_path
|
| 217 |
+
ext = Path(caminho).suffix.lower()
|
| 218 |
+
|
| 219 |
+
if ext == ".dai":
|
| 220 |
+
return load_dai_for_elaboracao(session, caminho)
|
| 221 |
+
|
| 222 |
+
abas, msg_abas, sucesso_abas = detectar_abas_excel(caminho)
|
| 223 |
+
if sucesso_abas and len(abas) > 1 and selected_sheet is None:
|
| 224 |
+
session.available_sheets = abas
|
| 225 |
+
return {
|
| 226 |
+
"status": msg_abas,
|
| 227 |
+
"requires_sheet": True,
|
| 228 |
+
"sheets": abas,
|
| 229 |
+
"sheet_selected": abas[0],
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
df, msg, sucesso = carregar_arquivo(caminho, selected_sheet)
|
| 233 |
+
if not sucesso or df is None:
|
| 234 |
+
raise HTTPException(status_code=400, detail=msg)
|
| 235 |
+
|
| 236 |
+
base = _set_dataframe_base(session, df, clear_models=True)
|
| 237 |
+
return {
|
| 238 |
+
"status": msg,
|
| 239 |
+
"requires_sheet": False,
|
| 240 |
+
"sheets": [],
|
| 241 |
+
**base,
|
| 242 |
+
"contexto": _selection_context(session),
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict[str, Any]:
|
| 247 |
+
(
|
| 248 |
+
df,
|
| 249 |
+
coluna_y,
|
| 250 |
+
colunas_x,
|
| 251 |
+
transformacao_y,
|
| 252 |
+
transformacoes_x,
|
| 253 |
+
dicotomicas,
|
| 254 |
+
codigo_alocado,
|
| 255 |
+
percentuais,
|
| 256 |
+
msg,
|
| 257 |
+
sucesso,
|
| 258 |
+
elaborador,
|
| 259 |
+
outliers_excluidos,
|
| 260 |
+
) = carregar_dai(caminho_arquivo)
|
| 261 |
+
|
| 262 |
+
if not sucesso or df is None:
|
| 263 |
+
raise HTTPException(status_code=400, detail=msg)
|
| 264 |
+
|
| 265 |
+
session.elaborador = elaborador
|
| 266 |
+
session.outliers_anteriores = _clean_int_list(outliers_excluidos)
|
| 267 |
+
|
| 268 |
+
base = _set_dataframe_base(session, df, clear_models=True)
|
| 269 |
+
|
| 270 |
+
selection_payload = apply_selection(
|
| 271 |
+
session,
|
| 272 |
+
coluna_y=coluna_y,
|
| 273 |
+
colunas_x=[str(c) for c in colunas_x],
|
| 274 |
+
dicotomicas=[str(c) for c in (dicotomicas or [])],
|
| 275 |
+
codigo_alocado=[str(c) for c in (codigo_alocado or [])],
|
| 276 |
+
percentuais=[str(c) for c in (percentuais or [])],
|
| 277 |
+
outliers_anteriores=session.outliers_anteriores,
|
| 278 |
+
grau_min_coef=1,
|
| 279 |
+
grau_min_f=0,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
fit_payload = fit_model(
|
| 283 |
+
session,
|
| 284 |
+
transformacao_y=transformacao_y,
|
| 285 |
+
transformacoes_x={str(k): str(v) for k, v in (transformacoes_x or {}).items()},
|
| 286 |
+
dicotomicas=[str(c) for c in (dicotomicas or [])],
|
| 287 |
+
codigo_alocado=[str(c) for c in (codigo_alocado or [])],
|
| 288 |
+
percentuais=[str(c) for c in (percentuais or [])],
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
html_outliers = ""
|
| 292 |
+
if session.outliers_anteriores:
|
| 293 |
+
lista = ", ".join(str(v) for v in session.outliers_anteriores)
|
| 294 |
+
html_outliers = formatar_outliers_anteriores_html(len(session.outliers_anteriores), lista)
|
| 295 |
+
|
| 296 |
+
return {
|
| 297 |
+
"status": msg,
|
| 298 |
+
"tipo": "dai",
|
| 299 |
+
**base,
|
| 300 |
+
"selection": selection_payload,
|
| 301 |
+
"fit": fit_payload,
|
| 302 |
+
"outliers_html": html_outliers,
|
| 303 |
+
"contexto": _selection_context(session),
|
| 304 |
+
"elaborador": sanitize_value(elaborador),
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def apply_selection(
|
| 309 |
+
session: SessionState,
|
| 310 |
+
coluna_y: str,
|
| 311 |
+
colunas_x: list[str],
|
| 312 |
+
dicotomicas: list[str] | None = None,
|
| 313 |
+
codigo_alocado: list[str] | None = None,
|
| 314 |
+
percentuais: list[str] | None = None,
|
| 315 |
+
outliers_anteriores: list[int] | None = None,
|
| 316 |
+
grau_min_coef: int = 1,
|
| 317 |
+
grau_min_f: int = 0,
|
| 318 |
+
) -> dict[str, Any]:
|
| 319 |
+
df = session.df_original
|
| 320 |
+
if df is None:
|
| 321 |
+
raise HTTPException(status_code=400, detail="Carregue um arquivo primeiro")
|
| 322 |
+
|
| 323 |
+
if coluna_y not in df.columns:
|
| 324 |
+
raise HTTPException(status_code=400, detail="Variavel Y invalida")
|
| 325 |
+
|
| 326 |
+
colunas_x_validas = [c for c in colunas_x if c in df.columns and c != coluna_y]
|
| 327 |
+
if not colunas_x_validas:
|
| 328 |
+
raise HTTPException(status_code=400, detail="Selecione ao menos uma variavel X")
|
| 329 |
+
|
| 330 |
+
outliers = _clean_int_list(outliers_anteriores if outliers_anteriores is not None else session.outliers_anteriores)
|
| 331 |
+
df_filtrado = df.copy()
|
| 332 |
+
if outliers:
|
| 333 |
+
df_filtrado = df_filtrado.drop(index=outliers, errors="ignore")
|
| 334 |
+
|
| 335 |
+
session.df_filtrado = df_filtrado
|
| 336 |
+
session.coluna_y = coluna_y
|
| 337 |
+
session.colunas_x = colunas_x_validas
|
| 338 |
+
|
| 339 |
+
dicotomicas = [c for c in (dicotomicas or []) if c in colunas_x_validas]
|
| 340 |
+
codigo_alocado = [c for c in (codigo_alocado or []) if c in colunas_x_validas]
|
| 341 |
+
percentuais = [c for c in (percentuais or []) if c in colunas_x_validas]
|
| 342 |
+
|
| 343 |
+
session.dicotomicas = dicotomicas
|
| 344 |
+
session.codigo_alocado = codigo_alocado
|
| 345 |
+
session.percentuais = percentuais
|
| 346 |
+
session.outliers_anteriores = outliers
|
| 347 |
+
session.iteracao = max(1, session.iteracao)
|
| 348 |
+
session.resultado_modelo = None
|
| 349 |
+
session.tabela_metricas_estado = None
|
| 350 |
+
session.avaliacoes_elaboracao = []
|
| 351 |
+
|
| 352 |
+
estatisticas = calcular_estatisticas_variaveis(df_filtrado, coluna_y, colunas=[coluna_y] + colunas_x_validas)
|
| 353 |
+
estatisticas = estatisticas.round(4)
|
| 354 |
+
session.tabela_estatisticas = estatisticas
|
| 355 |
+
|
| 356 |
+
resultado_micro = testar_micronumerosidade(
|
| 357 |
+
df_filtrado,
|
| 358 |
+
colunas_x_validas,
|
| 359 |
+
dicotomicas=dicotomicas,
|
| 360 |
+
codigo_alocado=codigo_alocado,
|
| 361 |
+
)
|
| 362 |
+
micro_html = formatar_micronumerosidade_html(resultado_micro)
|
| 363 |
+
|
| 364 |
+
try:
|
| 365 |
+
fig_dispersao = charts.criar_graficos_dispersao(df_filtrado[colunas_x_validas], df_filtrado[coluna_y])
|
| 366 |
+
except Exception:
|
| 367 |
+
fig_dispersao = None
|
| 368 |
+
|
| 369 |
+
busca_payload = search_transformacoes(session, grau_min_coef=grau_min_coef, grau_min_f=grau_min_f)
|
| 370 |
+
|
| 371 |
+
resultado_multi = verificar_multicolinearidade(df_filtrado, colunas_x_validas)
|
| 372 |
+
aviso_multi_html, aviso_multi_visivel = formatar_aviso_multicolinearidade(resultado_multi)
|
| 373 |
+
|
| 374 |
+
n_out = len(outliers)
|
| 375 |
+
resumo_outliers = f"Excluidos: {n_out} | A excluir: 0 | A reincluir: 0 | Total: {n_out}"
|
| 376 |
+
outliers_html = formatar_outliers_anteriores_html(
|
| 377 |
+
n_out,
|
| 378 |
+
", ".join(str(i) for i in outliers) if outliers else "",
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
transform_fields = []
|
| 382 |
+
existing = session.transformacoes_x if session.transformacoes_x else {}
|
| 383 |
+
for col in colunas_x_validas:
|
| 384 |
+
locked = col in dicotomicas or col in percentuais
|
| 385 |
+
transform_fields.append(
|
| 386 |
+
{
|
| 387 |
+
"coluna": col,
|
| 388 |
+
"locked": locked,
|
| 389 |
+
"valor": existing.get(col, "(x)"),
|
| 390 |
+
"choices": TRANSFORMACOES,
|
| 391 |
+
}
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
return {
|
| 395 |
+
"estatisticas": dataframe_to_payload(estatisticas, decimals=4),
|
| 396 |
+
"micronumerosidade_html": micro_html,
|
| 397 |
+
"grafico_dispersao": figure_to_payload(fig_dispersao),
|
| 398 |
+
"busca": busca_payload,
|
| 399 |
+
"resumo_outliers": resumo_outliers,
|
| 400 |
+
"outliers_html": outliers_html,
|
| 401 |
+
"aviso_multicolinearidade": {
|
| 402 |
+
"html": aviso_multi_html,
|
| 403 |
+
"visible": aviso_multi_visivel,
|
| 404 |
+
},
|
| 405 |
+
"transformacao_y": session.transformacao_y,
|
| 406 |
+
"transform_fields": transform_fields,
|
| 407 |
+
"mapa_html": charts.criar_mapa(df_filtrado),
|
| 408 |
+
"contexto": _selection_context(session),
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def search_transformacoes(session: SessionState, grau_min_coef: int = 1, grau_min_f: int = 0) -> dict[str, Any]:
|
| 413 |
+
df = session.df_filtrado if session.df_filtrado is not None else session.df_original
|
| 414 |
+
if df is None or not session.coluna_y or not session.colunas_x:
|
| 415 |
+
raise HTTPException(status_code=400, detail="Selecione variaveis antes de buscar transformacoes")
|
| 416 |
+
|
| 417 |
+
transformacoes_fixas: dict[str, str] = {}
|
| 418 |
+
for col in (session.dicotomicas or []) + (session.percentuais or []):
|
| 419 |
+
transformacoes_fixas[col] = "(x)"
|
| 420 |
+
|
| 421 |
+
resultados = buscar_melhores_transformacoes(
|
| 422 |
+
df,
|
| 423 |
+
session.coluna_y,
|
| 424 |
+
session.colunas_x,
|
| 425 |
+
transformacoes_fixas=transformacoes_fixas,
|
| 426 |
+
top_n=5,
|
| 427 |
+
grau_min_coef=int(grau_min_coef),
|
| 428 |
+
grau_min_f=int(grau_min_f),
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
session.resultados_busca = sanitize_value(resultados)
|
| 432 |
+
|
| 433 |
+
if not resultados:
|
| 434 |
+
html = (
|
| 435 |
+
"<p style='color: orange;'><b>Aviso:</b> Nenhuma combinacao encontrada com os criterios atuais.</p>"
|
| 436 |
+
)
|
| 437 |
+
return {
|
| 438 |
+
"html": html,
|
| 439 |
+
"resultados": [],
|
| 440 |
+
"grau_coef": int(grau_min_coef),
|
| 441 |
+
"grau_f": int(grau_min_f),
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
html = formatar_busca_html(resultados)
|
| 445 |
+
|
| 446 |
+
grau_coef_usado = min(
|
| 447 |
+
min(resultado.get("graus_coef", {}).values(), default=0) for resultado in resultados
|
| 448 |
+
)
|
| 449 |
+
grau_f_usado = min(resultado.get("grau_f", 0) for resultado in resultados)
|
| 450 |
+
|
| 451 |
+
return {
|
| 452 |
+
"html": html,
|
| 453 |
+
"resultados": sanitize_value(resultados),
|
| 454 |
+
"grau_coef": int(grau_coef_usado),
|
| 455 |
+
"grau_f": int(grau_f_usado),
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def adotar_sugestao(session: SessionState, indice: int) -> dict[str, Any]:
|
| 460 |
+
if not session.resultados_busca:
|
| 461 |
+
raise HTTPException(status_code=400, detail="Nao ha sugestoes carregadas")
|
| 462 |
+
|
| 463 |
+
if indice < 0 or indice >= len(session.resultados_busca):
|
| 464 |
+
raise HTTPException(status_code=400, detail="Indice de sugestao invalido")
|
| 465 |
+
|
| 466 |
+
sugestao = session.resultados_busca[indice]
|
| 467 |
+
session.transformacao_y = str(sugestao.get("transformacao_y", "(x)"))
|
| 468 |
+
transf = sugestao.get("transformacoes_x", {}) or {}
|
| 469 |
+
session.transformacoes_x = {str(k): str(v) for k, v in transf.items()}
|
| 470 |
+
|
| 471 |
+
return {
|
| 472 |
+
"transformacao_y": session.transformacao_y,
|
| 473 |
+
"transformacoes_x": session.transformacoes_x,
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def fit_model(
|
| 478 |
+
session: SessionState,
|
| 479 |
+
transformacao_y: str,
|
| 480 |
+
transformacoes_x: dict[str, str] | None,
|
| 481 |
+
dicotomicas: list[str] | None = None,
|
| 482 |
+
codigo_alocado: list[str] | None = None,
|
| 483 |
+
percentuais: list[str] | None = None,
|
| 484 |
+
) -> dict[str, Any]:
|
| 485 |
+
if session.df_filtrado is None:
|
| 486 |
+
raise HTTPException(status_code=400, detail="Aplique a selecao antes de ajustar")
|
| 487 |
+
if not session.coluna_y or not session.colunas_x:
|
| 488 |
+
raise HTTPException(status_code=400, detail="Variaveis Y/X nao definidas")
|
| 489 |
+
|
| 490 |
+
session.transformacao_y = transformacao_y or "(x)"
|
| 491 |
+
|
| 492 |
+
transf_map: dict[str, str] = {}
|
| 493 |
+
incoming = transformacoes_x or {}
|
| 494 |
+
for col in session.colunas_x:
|
| 495 |
+
transf_map[col] = str(incoming.get(col, "(x)"))
|
| 496 |
+
session.transformacoes_x = transf_map
|
| 497 |
+
|
| 498 |
+
session.dicotomicas = [c for c in (dicotomicas or session.dicotomicas) if c in session.colunas_x]
|
| 499 |
+
session.codigo_alocado = [c for c in (codigo_alocado or session.codigo_alocado) if c in session.colunas_x]
|
| 500 |
+
session.percentuais = [c for c in (percentuais or session.percentuais) if c in session.colunas_x]
|
| 501 |
+
|
| 502 |
+
resultado = ajustar_modelo(
|
| 503 |
+
session.df_filtrado,
|
| 504 |
+
session.coluna_y,
|
| 505 |
+
session.colunas_x,
|
| 506 |
+
session.transformacao_y,
|
| 507 |
+
session.transformacoes_x,
|
| 508 |
+
)
|
| 509 |
+
if resultado is None:
|
| 510 |
+
raise HTTPException(status_code=400, detail="Nao foi possivel ajustar o modelo")
|
| 511 |
+
|
| 512 |
+
resultado["dicotomicas"] = list(session.dicotomicas)
|
| 513 |
+
resultado["codigo_alocado"] = list(session.codigo_alocado)
|
| 514 |
+
resultado["percentuais"] = list(session.percentuais)
|
| 515 |
+
|
| 516 |
+
session.resultado_modelo = resultado
|
| 517 |
+
session.avaliacoes_elaboracao = []
|
| 518 |
+
|
| 519 |
+
diagnosticos_html = formatar_diagnosticos_html(resultado["diagnosticos"])
|
| 520 |
+
|
| 521 |
+
try:
|
| 522 |
+
fig_dispersao_transf = charts.criar_graficos_dispersao(
|
| 523 |
+
resultado["X_transformado"],
|
| 524 |
+
resultado["y_transformado"],
|
| 525 |
+
)
|
| 526 |
+
except Exception:
|
| 527 |
+
fig_dispersao_transf = None
|
| 528 |
+
|
| 529 |
+
fig_corr = None
|
| 530 |
+
try:
|
| 531 |
+
df_corr = resultado["X_transformado"].copy()
|
| 532 |
+
df_corr[session.coluna_y] = resultado["y_transformado"]
|
| 533 |
+
fig_corr = charts.criar_matriz_correlacao(df_corr, [session.coluna_y] + list(session.colunas_x))
|
| 534 |
+
except Exception:
|
| 535 |
+
fig_corr = None
|
| 536 |
+
|
| 537 |
+
graficos = charts.criar_painel_diagnostico(resultado)
|
| 538 |
+
|
| 539 |
+
tabela_metricas = resultado["tabela_obs_calc"].copy()
|
| 540 |
+
tabela_metricas_estado = tabela_metricas.set_index("Índice")
|
| 541 |
+
|
| 542 |
+
indices_usados = resultado.get("indices_usados", [])
|
| 543 |
+
if indices_usados:
|
| 544 |
+
df_base = session.df_filtrado.loc[indices_usados]
|
| 545 |
+
else:
|
| 546 |
+
df_base = session.df_filtrado
|
| 547 |
+
|
| 548 |
+
colunas_novas = {
|
| 549 |
+
col: df_base[col].values
|
| 550 |
+
for col in df_base.columns
|
| 551 |
+
if col not in tabela_metricas_estado.columns
|
| 552 |
+
}
|
| 553 |
+
if colunas_novas:
|
| 554 |
+
tabela_metricas_estado = pd.concat(
|
| 555 |
+
[
|
| 556 |
+
tabela_metricas_estado,
|
| 557 |
+
pd.DataFrame(colunas_novas, index=tabela_metricas_estado.index),
|
| 558 |
+
],
|
| 559 |
+
axis=1,
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
session.tabela_metricas_estado = tabela_metricas_estado
|
| 563 |
+
|
| 564 |
+
colunas_originais = [c for c in session.df_filtrado.columns if c not in {"Observado", "Calculado", "Resíduo", "Resíduo Pad.", "Resíduo Stud.", "Cook"}]
|
| 565 |
+
variaveis_filtro = ["Resíduo Pad.", "Resíduo Stud.", "Cook"] + [str(c) for c in colunas_originais]
|
| 566 |
+
|
| 567 |
+
n_out = len(session.outliers_anteriores)
|
| 568 |
+
resumo = f"Excluidos: {n_out} | A excluir: 0 | A reincluir: 0 | Total: {n_out}"
|
| 569 |
+
|
| 570 |
+
return {
|
| 571 |
+
"diagnosticos_html": diagnosticos_html,
|
| 572 |
+
"tabela_coef": dataframe_to_payload(resultado["tabela_coef"], decimals=4),
|
| 573 |
+
"tabela_obs_calc": dataframe_to_payload(resultado["tabela_obs_calc"], decimals=4),
|
| 574 |
+
"grafico_dispersao_modelo": figure_to_payload(fig_dispersao_transf),
|
| 575 |
+
"grafico_obs_calc": figure_to_payload(graficos.get("obs_calc")),
|
| 576 |
+
"grafico_residuos": figure_to_payload(graficos.get("residuos")),
|
| 577 |
+
"grafico_histograma": figure_to_payload(graficos.get("histograma")),
|
| 578 |
+
"grafico_cook": figure_to_payload(graficos.get("cook")),
|
| 579 |
+
"grafico_correlacao": figure_to_payload(fig_corr),
|
| 580 |
+
"tabela_metricas": dataframe_to_payload(tabela_metricas, decimals=4),
|
| 581 |
+
"variaveis_filtro": variaveis_filtro,
|
| 582 |
+
"resumo_outliers": resumo,
|
| 583 |
+
"avaliacao_campos": build_campos_avaliacao(session),
|
| 584 |
+
"contexto": _selection_context(session),
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def gerar_grafico_dispersao_modelo(session: SessionState, tipo: str) -> dict[str, Any]:
|
| 589 |
+
if not session.resultado_modelo:
|
| 590 |
+
raise HTTPException(status_code=400, detail="Ajuste um modelo primeiro")
|
| 591 |
+
|
| 592 |
+
try:
|
| 593 |
+
if "Residuo" in tipo or "Resíduo" in tipo:
|
| 594 |
+
tabela = session.resultado_modelo.get("tabela_obs_calc")
|
| 595 |
+
residuos = tabela["Resíduo Pad."].values if tabela is not None and "Resíduo Pad." in tabela.columns else None
|
| 596 |
+
fig = charts.criar_graficos_dispersao_residuos(session.resultado_modelo["X_transformado"], residuos)
|
| 597 |
+
else:
|
| 598 |
+
fig = charts.criar_graficos_dispersao(
|
| 599 |
+
session.resultado_modelo["X_transformado"],
|
| 600 |
+
session.resultado_modelo["y_transformado"],
|
| 601 |
+
)
|
| 602 |
+
except Exception:
|
| 603 |
+
fig = None
|
| 604 |
+
|
| 605 |
+
return {"grafico": figure_to_payload(fig)}
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def apply_outlier_filters(session: SessionState, filtros: list[dict[str, Any]]) -> dict[str, Any]:
|
| 609 |
+
metricas = session.tabela_metricas_estado
|
| 610 |
+
if metricas is None:
|
| 611 |
+
raise HTTPException(status_code=400, detail="Ajuste o modelo para gerar metricas")
|
| 612 |
+
|
| 613 |
+
if not filtros:
|
| 614 |
+
return {"indices": [], "texto": ""}
|
| 615 |
+
|
| 616 |
+
indices_outliers: set[int] = set()
|
| 617 |
+
|
| 618 |
+
for filtro in filtros:
|
| 619 |
+
var = str(filtro.get("variavel", "")).strip()
|
| 620 |
+
op = str(filtro.get("operador", "")).strip()
|
| 621 |
+
val = filtro.get("valor")
|
| 622 |
+
|
| 623 |
+
if not var or var not in metricas.columns:
|
| 624 |
+
continue
|
| 625 |
+
if val is None:
|
| 626 |
+
continue
|
| 627 |
+
|
| 628 |
+
try:
|
| 629 |
+
val_num = float(val)
|
| 630 |
+
except Exception:
|
| 631 |
+
continue
|
| 632 |
+
|
| 633 |
+
if op == "<=":
|
| 634 |
+
mask = metricas[var] <= val_num
|
| 635 |
+
elif op == ">=":
|
| 636 |
+
mask = metricas[var] >= val_num
|
| 637 |
+
elif op == "<":
|
| 638 |
+
mask = metricas[var] < val_num
|
| 639 |
+
elif op == ">":
|
| 640 |
+
mask = metricas[var] > val_num
|
| 641 |
+
elif op == "=":
|
| 642 |
+
mask = metricas[var] == val_num
|
| 643 |
+
else:
|
| 644 |
+
continue
|
| 645 |
+
|
| 646 |
+
matched = metricas[mask].index.tolist()
|
| 647 |
+
for idx in matched:
|
| 648 |
+
try:
|
| 649 |
+
indices_outliers.add(int(idx))
|
| 650 |
+
except Exception:
|
| 651 |
+
continue
|
| 652 |
+
|
| 653 |
+
indices = sorted(indices_outliers)
|
| 654 |
+
texto = ", ".join(str(i) for i in indices)
|
| 655 |
+
return {"indices": indices, "texto": texto}
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def resumir_outliers(outliers_anteriores: list[int], outliers_texto: str | None, reincluir_texto: str | None) -> str:
|
| 659 |
+
anteriores = _clean_int_list(outliers_anteriores)
|
| 660 |
+
novos = _parse_indices_text(outliers_texto)
|
| 661 |
+
reincluir = _parse_indices_text(reincluir_texto)
|
| 662 |
+
|
| 663 |
+
anteriores_atualizados = [i for i in anteriores if i not in reincluir]
|
| 664 |
+
total = sorted(set(anteriores_atualizados + novos))
|
| 665 |
+
return (
|
| 666 |
+
f"Excluidos: {len(anteriores)} | A excluir: {len(novos)} | "
|
| 667 |
+
f"A reincluir: {len(reincluir)} | Total: {len(total)}"
|
| 668 |
+
)
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
def reiniciar_iteracao(
|
| 672 |
+
session: SessionState,
|
| 673 |
+
outliers_texto: str | None,
|
| 674 |
+
reincluir_texto: str | None,
|
| 675 |
+
grau_min_coef: int = 3,
|
| 676 |
+
grau_min_f: int = 3,
|
| 677 |
+
) -> dict[str, Any]:
|
| 678 |
+
if session.df_original is None:
|
| 679 |
+
raise HTTPException(status_code=400, detail="Carregue dados primeiro")
|
| 680 |
+
if not session.coluna_y or not session.colunas_x:
|
| 681 |
+
raise HTTPException(status_code=400, detail="Selecione variaveis primeiro")
|
| 682 |
+
|
| 683 |
+
novos = _parse_indices_text(outliers_texto)
|
| 684 |
+
reincluir = _parse_indices_text(reincluir_texto)
|
| 685 |
+
|
| 686 |
+
anteriores_atualizados = [i for i in session.outliers_anteriores if i not in reincluir]
|
| 687 |
+
outliers_combinados = sorted(set(anteriores_atualizados + novos))
|
| 688 |
+
|
| 689 |
+
session.outliers_anteriores = outliers_combinados
|
| 690 |
+
session.iteracao = (session.iteracao or 1) + 1
|
| 691 |
+
|
| 692 |
+
selection = apply_selection(
|
| 693 |
+
session,
|
| 694 |
+
coluna_y=session.coluna_y,
|
| 695 |
+
colunas_x=session.colunas_x,
|
| 696 |
+
dicotomicas=session.dicotomicas,
|
| 697 |
+
codigo_alocado=session.codigo_alocado,
|
| 698 |
+
percentuais=session.percentuais,
|
| 699 |
+
outliers_anteriores=outliers_combinados,
|
| 700 |
+
grau_min_coef=int(grau_min_coef),
|
| 701 |
+
grau_min_f=int(grau_min_f),
|
| 702 |
+
)
|
| 703 |
+
|
| 704 |
+
html_outliers = formatar_outliers_anteriores_html(
|
| 705 |
+
len(outliers_combinados),
|
| 706 |
+
", ".join(str(i) for i in outliers_combinados) if outliers_combinados else "",
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
selection.update(
|
| 710 |
+
{
|
| 711 |
+
"outliers_anteriores": outliers_combinados,
|
| 712 |
+
"iteracao": session.iteracao,
|
| 713 |
+
"outliers_html": html_outliers,
|
| 714 |
+
"resumo_outliers": resumir_outliers(outliers_combinados, "", ""),
|
| 715 |
+
}
|
| 716 |
+
)
|
| 717 |
+
|
| 718 |
+
return selection
|
| 719 |
+
|
| 720 |
+
|
| 721 |
+
def build_campos_avaliacao(session: SessionState) -> list[dict[str, Any]]:
|
| 722 |
+
if session.resultado_modelo is None or session.tabela_estatisticas is None:
|
| 723 |
+
return []
|
| 724 |
+
|
| 725 |
+
colunas_x = list(session.resultado_modelo.get("colunas_x", []))
|
| 726 |
+
|
| 727 |
+
est = session.tabela_estatisticas
|
| 728 |
+
if "Variável" in est.columns:
|
| 729 |
+
est_idx = est.set_index("Variável")
|
| 730 |
+
else:
|
| 731 |
+
est_idx = est
|
| 732 |
+
|
| 733 |
+
campos: list[dict[str, Any]] = []
|
| 734 |
+
for col in colunas_x:
|
| 735 |
+
placeholder = ""
|
| 736 |
+
if col in session.dicotomicas:
|
| 737 |
+
placeholder = "0 ou 1"
|
| 738 |
+
tipo = "dicotomica"
|
| 739 |
+
elif col in session.codigo_alocado and col in est_idx.index:
|
| 740 |
+
min_v = est_idx.loc[col, "Mínimo"]
|
| 741 |
+
max_v = est_idx.loc[col, "Máximo"]
|
| 742 |
+
placeholder = f"cod. {int(min_v)} a {int(max_v)}"
|
| 743 |
+
tipo = "codigo_alocado"
|
| 744 |
+
elif col in session.percentuais:
|
| 745 |
+
placeholder = "0 a 1"
|
| 746 |
+
tipo = "percentual"
|
| 747 |
+
elif col in est_idx.index:
|
| 748 |
+
min_v = est_idx.loc[col, "Mínimo"]
|
| 749 |
+
max_v = est_idx.loc[col, "Máximo"]
|
| 750 |
+
placeholder = f"{min_v} — {max_v}"
|
| 751 |
+
tipo = "numerica"
|
| 752 |
+
else:
|
| 753 |
+
tipo = "numerica"
|
| 754 |
+
|
| 755 |
+
campos.append(
|
| 756 |
+
{
|
| 757 |
+
"coluna": col,
|
| 758 |
+
"placeholder": placeholder,
|
| 759 |
+
"tipo": tipo,
|
| 760 |
+
}
|
| 761 |
+
)
|
| 762 |
+
|
| 763 |
+
return sanitize_value(campos)
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def calcular_avaliacao_elaboracao(
|
| 767 |
+
session: SessionState,
|
| 768 |
+
valores_x: dict[str, Any],
|
| 769 |
+
indice_base: str | None,
|
| 770 |
+
) -> dict[str, Any]:
|
| 771 |
+
if session.resultado_modelo is None:
|
| 772 |
+
raise HTTPException(status_code=400, detail="Ajuste um modelo primeiro")
|
| 773 |
+
if session.tabela_estatisticas is None:
|
| 774 |
+
raise HTTPException(status_code=400, detail="Estatisticas indisponiveis")
|
| 775 |
+
|
| 776 |
+
colunas_x = list(session.resultado_modelo.get("colunas_x", []))
|
| 777 |
+
if not colunas_x:
|
| 778 |
+
raise HTTPException(status_code=400, detail="Modelo sem variaveis")
|
| 779 |
+
|
| 780 |
+
entradas: dict[str, float] = {}
|
| 781 |
+
for col in colunas_x:
|
| 782 |
+
if col not in valores_x:
|
| 783 |
+
raise HTTPException(status_code=400, detail=f"Valor ausente para {col}")
|
| 784 |
+
try:
|
| 785 |
+
entradas[col] = float(valores_x[col])
|
| 786 |
+
except Exception as exc:
|
| 787 |
+
raise HTTPException(status_code=400, detail=f"Valor invalido para {col}") from exc
|
| 788 |
+
|
| 789 |
+
est = session.tabela_estatisticas
|
| 790 |
+
if "Variável" in est.columns:
|
| 791 |
+
est_idx = est.set_index("Variável")
|
| 792 |
+
else:
|
| 793 |
+
est_idx = est
|
| 794 |
+
|
| 795 |
+
for col in colunas_x:
|
| 796 |
+
valor = entradas[col]
|
| 797 |
+
if col in session.dicotomicas and valor not in (0, 0.0, 1, 1.0):
|
| 798 |
+
raise HTTPException(status_code=400, detail=f"{col} aceita apenas 0 ou 1")
|
| 799 |
+
if col in session.codigo_alocado and col in est_idx.index:
|
| 800 |
+
min_v = float(est_idx.loc[col, "Mínimo"])
|
| 801 |
+
max_v = float(est_idx.loc[col, "Máximo"])
|
| 802 |
+
if float(valor) != int(float(valor)) or valor < min_v or valor > max_v:
|
| 803 |
+
raise HTTPException(status_code=400, detail=f"{col} aceita inteiros de {int(min_v)} a {int(max_v)}")
|
| 804 |
+
if col in session.percentuais and (valor < 0 or valor > 1):
|
| 805 |
+
raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
|
| 806 |
+
|
| 807 |
+
resultado = avaliar_imovel(
|
| 808 |
+
modelo_sm=session.resultado_modelo["modelo_sm"],
|
| 809 |
+
valores_x=entradas,
|
| 810 |
+
colunas_x=colunas_x,
|
| 811 |
+
transformacoes_x=session.resultado_modelo.get("transformacoes_x", {}),
|
| 812 |
+
transformacao_y=session.resultado_modelo.get("transformacao_y", "(x)"),
|
| 813 |
+
estatisticas_df=session.tabela_estatisticas,
|
| 814 |
+
dicotomicas=session.dicotomicas,
|
| 815 |
+
codigo_alocado=session.codigo_alocado,
|
| 816 |
+
percentuais=session.percentuais,
|
| 817 |
+
)
|
| 818 |
+
if resultado is None:
|
| 819 |
+
raise HTTPException(status_code=400, detail="Erro ao calcular avaliacao")
|
| 820 |
+
|
| 821 |
+
session.avaliacoes_elaboracao.append(resultado)
|
| 822 |
+
idx_base = int(indice_base) - 1 if indice_base else 0
|
| 823 |
+
html = formatar_avaliacao_html(session.avaliacoes_elaboracao, indice_base=idx_base, elem_id_excluir="excluir-aval-elab")
|
| 824 |
+
|
| 825 |
+
choices = [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))]
|
| 826 |
+
base = indice_base if indice_base else "1"
|
| 827 |
+
|
| 828 |
+
return {
|
| 829 |
+
"resultado_html": html,
|
| 830 |
+
"avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
|
| 831 |
+
"base_choices": choices,
|
| 832 |
+
"base_value": base,
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def limpar_avaliacoes_elaboracao(session: SessionState) -> dict[str, Any]:
|
| 837 |
+
session.avaliacoes_elaboracao = []
|
| 838 |
+
return {
|
| 839 |
+
"resultado_html": "",
|
| 840 |
+
"avaliacoes": [],
|
| 841 |
+
"base_choices": [],
|
| 842 |
+
"base_value": None,
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
|
| 846 |
+
def excluir_avaliacao_elaboracao(session: SessionState, indice_str: str | None, indice_base_str: str | None) -> dict[str, Any]:
|
| 847 |
+
if not indice_str or not session.avaliacoes_elaboracao:
|
| 848 |
+
return {
|
| 849 |
+
"resultado_html": None,
|
| 850 |
+
"avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
|
| 851 |
+
"base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
|
| 852 |
+
"base_value": indice_base_str,
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
try:
|
| 856 |
+
idx = int(indice_str.strip()) - 1
|
| 857 |
+
except Exception:
|
| 858 |
+
return {
|
| 859 |
+
"resultado_html": None,
|
| 860 |
+
"avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
|
| 861 |
+
"base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
|
| 862 |
+
"base_value": indice_base_str,
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
if idx < 0 or idx >= len(session.avaliacoes_elaboracao):
|
| 866 |
+
return {
|
| 867 |
+
"resultado_html": None,
|
| 868 |
+
"avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
|
| 869 |
+
"base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
|
| 870 |
+
"base_value": indice_base_str,
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
nova_lista = [a for i, a in enumerate(session.avaliacoes_elaboracao) if i != idx]
|
| 874 |
+
session.avaliacoes_elaboracao = nova_lista
|
| 875 |
+
|
| 876 |
+
if not nova_lista:
|
| 877 |
+
return {
|
| 878 |
+
"resultado_html": "",
|
| 879 |
+
"avaliacoes": [],
|
| 880 |
+
"base_choices": [],
|
| 881 |
+
"base_value": None,
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 885 |
+
if base >= len(nova_lista):
|
| 886 |
+
base = len(nova_lista) - 1
|
| 887 |
+
if base < 0:
|
| 888 |
+
base = 0
|
| 889 |
+
|
| 890 |
+
html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-elab")
|
| 891 |
+
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 892 |
+
|
| 893 |
+
return {
|
| 894 |
+
"resultado_html": html,
|
| 895 |
+
"avaliacoes": sanitize_value(nova_lista),
|
| 896 |
+
"base_choices": choices,
|
| 897 |
+
"base_value": str(base + 1),
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
|
| 901 |
+
def atualizar_base_avaliacao_elaboracao(session: SessionState, indice_base_str: str | None) -> dict[str, Any]:
|
| 902 |
+
if not session.avaliacoes_elaboracao:
|
| 903 |
+
return {"resultado_html": ""}
|
| 904 |
+
indice = int(indice_base_str) - 1 if indice_base_str else 0
|
| 905 |
+
html = formatar_avaliacao_html(session.avaliacoes_elaboracao, indice_base=indice, elem_id_excluir="excluir-aval-elab")
|
| 906 |
+
return {"resultado_html": html}
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
def exportar_avaliacoes_elaboracao(session: SessionState) -> str:
|
| 910 |
+
caminho = exportar_avaliacoes_excel(session.avaliacoes_elaboracao)
|
| 911 |
+
if not caminho:
|
| 912 |
+
raise HTTPException(status_code=400, detail="Sem avaliacoes para exportar")
|
| 913 |
+
return caminho
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[str, Any] | None = None) -> tuple[str, str]:
|
| 917 |
+
if session.resultado_modelo is None:
|
| 918 |
+
raise HTTPException(status_code=400, detail="Ajuste um modelo antes de exportar")
|
| 919 |
+
if session.df_filtrado is None or session.df_original is None:
|
| 920 |
+
raise HTTPException(status_code=400, detail="Dados nao disponiveis para exportacao")
|
| 921 |
+
|
| 922 |
+
if not nome_arquivo or not nome_arquivo.strip():
|
| 923 |
+
raise HTTPException(status_code=400, detail="Informe o nome do arquivo")
|
| 924 |
+
|
| 925 |
+
caminho, msg = exportar_modelo_dai(
|
| 926 |
+
session.resultado_modelo,
|
| 927 |
+
session.df_filtrado,
|
| 928 |
+
session.df_original,
|
| 929 |
+
session.tabela_estatisticas,
|
| 930 |
+
nome_arquivo.strip(),
|
| 931 |
+
elaborador=elaborador,
|
| 932 |
+
outliers_excluidos=session.outliers_anteriores,
|
| 933 |
+
)
|
| 934 |
+
|
| 935 |
+
if not caminho:
|
| 936 |
+
raise HTTPException(status_code=400, detail=msg)
|
| 937 |
+
|
| 938 |
+
return caminho, msg
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
def exportar_base(session: SessionState, usar_filtrado: bool = True) -> str:
|
| 942 |
+
df = session.df_filtrado if usar_filtrado else session.df_original
|
| 943 |
+
caminho = exportar_base_csv(df)
|
| 944 |
+
if not caminho:
|
| 945 |
+
raise HTTPException(status_code=400, detail="Base indisponivel")
|
| 946 |
+
return caminho
|
| 947 |
+
|
| 948 |
+
|
| 949 |
+
def atualizar_mapa(session: SessionState, var_mapa: str | None) -> dict[str, Any]:
|
| 950 |
+
df = session.df_filtrado if session.df_filtrado is not None else session.df_original
|
| 951 |
+
if df is None:
|
| 952 |
+
raise HTTPException(status_code=400, detail="Carregue dados primeiro")
|
| 953 |
+
|
| 954 |
+
tamanho_col = None if not var_mapa or var_mapa == "Visualizacao Padrao" or var_mapa == "Visualização Padrão" else var_mapa
|
| 955 |
+
mapa_html = charts.criar_mapa(df, tamanho_col=tamanho_col)
|
| 956 |
+
return {"mapa_html": mapa_html}
|
| 957 |
+
|
| 958 |
+
|
| 959 |
+
def mapear_coordenadas_manualmente(session: SessionState, col_lat: str, col_lon: str) -> dict[str, Any]:
|
| 960 |
+
df = session.df_original
|
| 961 |
+
if df is None:
|
| 962 |
+
raise HTTPException(status_code=400, detail="Carregue dados primeiro")
|
| 963 |
+
if not col_lat or not col_lon:
|
| 964 |
+
raise HTTPException(status_code=400, detail="Selecione latitude e longitude")
|
| 965 |
+
|
| 966 |
+
n_total = len(df)
|
| 967 |
+
if n_total == 0:
|
| 968 |
+
raise HTTPException(status_code=400, detail="DataFrame vazio")
|
| 969 |
+
|
| 970 |
+
lat_num = pd.to_numeric(df[col_lat], errors="coerce")
|
| 971 |
+
lon_num = pd.to_numeric(df[col_lon], errors="coerce")
|
| 972 |
+
lat_vals = lat_num.dropna()
|
| 973 |
+
lon_vals = lon_num.dropna()
|
| 974 |
+
|
| 975 |
+
lat_conv = len(lat_vals) / n_total
|
| 976 |
+
lon_conv = len(lon_vals) / n_total
|
| 977 |
+
|
| 978 |
+
erros: list[str] = []
|
| 979 |
+
if lat_conv < 0.5:
|
| 980 |
+
erros.append(f"{col_lat}: apenas {lat_conv:.0%} numericos")
|
| 981 |
+
if lon_conv < 0.5:
|
| 982 |
+
erros.append(f"{col_lon}: apenas {lon_conv:.0%} numericos")
|
| 983 |
+
|
| 984 |
+
if not erros:
|
| 985 |
+
lat_in_range = ((lat_vals >= -90) & (lat_vals <= 90)).mean()
|
| 986 |
+
lon_in_range = ((lon_vals >= -180) & (lon_vals <= 180)).mean()
|
| 987 |
+
lat_in_lon_range = ((lat_vals >= -180) & (lat_vals <= 180)).mean()
|
| 988 |
+
lon_in_lat_range = ((lon_vals >= -90) & (lon_vals <= 90)).mean()
|
| 989 |
+
|
| 990 |
+
inversao = lat_in_range < 0.5 and lat_in_lon_range > 0.8 and lon_in_lat_range > 0.8
|
| 991 |
+
if inversao:
|
| 992 |
+
erros.append("As colunas parecem invertidas")
|
| 993 |
+
else:
|
| 994 |
+
if lat_in_range < 0.8:
|
| 995 |
+
erros.append(f"{col_lat}: baixa aderencia ao intervalo de latitude")
|
| 996 |
+
if lon_in_range < 0.8:
|
| 997 |
+
erros.append(f"{col_lon}: baixa aderencia ao intervalo de longitude")
|
| 998 |
+
|
| 999 |
+
if not erros:
|
| 1000 |
+
lat_tem_decimais = (lat_vals % 1 != 0).mean() if len(lat_vals) > 0 else 0.0
|
| 1001 |
+
lon_tem_decimais = (lon_vals % 1 != 0).mean() if len(lon_vals) > 0 else 0.0
|
| 1002 |
+
if lat_tem_decimais < 0.1:
|
| 1003 |
+
erros.append(f"{col_lat}: poucos valores com casas decimais")
|
| 1004 |
+
if lon_tem_decimais < 0.1:
|
| 1005 |
+
erros.append(f"{col_lon}: poucos valores com casas decimais")
|
| 1006 |
+
|
| 1007 |
+
if erros:
|
| 1008 |
+
raise HTTPException(status_code=400, detail="; ".join(erros))
|
| 1009 |
+
|
| 1010 |
+
df_novo = geocodificacao.padronizar_coords(df, col_lat, col_lon)
|
| 1011 |
+
session.df_original = df_novo
|
| 1012 |
+
|
| 1013 |
+
df_filtrado = df_novo.copy()
|
| 1014 |
+
if session.outliers_anteriores:
|
| 1015 |
+
df_filtrado = df_filtrado.drop(index=session.outliers_anteriores, errors="ignore")
|
| 1016 |
+
session.df_filtrado = df_filtrado
|
| 1017 |
+
|
| 1018 |
+
return {
|
| 1019 |
+
"status": "Coordenadas mapeadas com sucesso",
|
| 1020 |
+
"mapa_html": charts.criar_mapa(df_filtrado),
|
| 1021 |
+
"dados": dataframe_to_payload(df_filtrado, decimals=4),
|
| 1022 |
+
"coords": _build_coords_payload(df_novo, True),
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
|
| 1026 |
+
def geocodificar(session: SessionState, col_cdlog: str, col_num: str, auto_200: bool = False) -> dict[str, Any]:
|
| 1027 |
+
if session.df_original is None:
|
| 1028 |
+
raise HTTPException(status_code=400, detail="Carregue dados primeiro")
|
| 1029 |
+
|
| 1030 |
+
try:
|
| 1031 |
+
df_resultado, df_falhas, ajustados = geocodificacao.geocodificar(session.df_original, col_cdlog, col_num, auto_200=auto_200)
|
| 1032 |
+
except Exception as exc:
|
| 1033 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 1034 |
+
|
| 1035 |
+
session.df_original = df_resultado
|
| 1036 |
+
session.df_filtrado = df_resultado.drop(index=session.outliers_anteriores, errors="ignore")
|
| 1037 |
+
session.geo_falhas_df = df_falhas
|
| 1038 |
+
session.geo_col_cdlog = col_cdlog
|
| 1039 |
+
session.geo_col_num = col_num
|
| 1040 |
+
|
| 1041 |
+
status_html = geocodificacao.formatar_status_geocodificacao(df_resultado, df_falhas, ajustados)
|
| 1042 |
+
falhas_html, df_correcoes = geocodificacao.preparar_display_falhas(df_falhas)
|
| 1043 |
+
|
| 1044 |
+
return {
|
| 1045 |
+
"status_html": status_html,
|
| 1046 |
+
"falhas_html": falhas_html,
|
| 1047 |
+
"falhas_para_correcao": dataframe_to_payload(df_correcoes, decimals=None),
|
| 1048 |
+
"mapa_html": charts.criar_mapa(session.df_filtrado),
|
| 1049 |
+
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 1050 |
+
"coords": _build_coords_payload(session.df_original, True),
|
| 1051 |
+
}
|
| 1052 |
+
|
| 1053 |
+
|
| 1054 |
+
def aplicar_correcoes_geocodificacao(
|
| 1055 |
+
session: SessionState,
|
| 1056 |
+
correcoes: list[dict[str, Any]],
|
| 1057 |
+
auto_200: bool = False,
|
| 1058 |
+
) -> dict[str, Any]:
|
| 1059 |
+
if session.df_original is None:
|
| 1060 |
+
raise HTTPException(status_code=400, detail="Carregue dados primeiro")
|
| 1061 |
+
if session.geo_falhas_df is None:
|
| 1062 |
+
raise HTTPException(status_code=400, detail="Nao ha falhas de geocodificacao pendentes")
|
| 1063 |
+
if not session.geo_col_cdlog or not session.geo_col_num:
|
| 1064 |
+
raise HTTPException(status_code=400, detail="Contexto de geocodificacao ausente")
|
| 1065 |
+
|
| 1066 |
+
df_falhas = session.geo_falhas_df.copy()
|
| 1067 |
+
mapa_correcao = {
|
| 1068 |
+
int(item.get("linha")): str(item.get("numero_corrigido", "")).strip()
|
| 1069 |
+
for item in correcoes
|
| 1070 |
+
if item.get("linha") is not None
|
| 1071 |
+
}
|
| 1072 |
+
|
| 1073 |
+
if "numero_corrigido" not in df_falhas.columns:
|
| 1074 |
+
df_falhas["numero_corrigido"] = ""
|
| 1075 |
+
|
| 1076 |
+
for idx, row in df_falhas.iterrows():
|
| 1077 |
+
linha = int(row.get("_idx"))
|
| 1078 |
+
valor = mapa_correcao.get(linha, "")
|
| 1079 |
+
df_falhas.at[idx, "numero_corrigido"] = valor
|
| 1080 |
+
|
| 1081 |
+
try:
|
| 1082 |
+
df_resultado, df_falhas_novas, ajustados, manuais = geocodificacao.aplicar_correcoes_e_regeodificar(
|
| 1083 |
+
session.df_original,
|
| 1084 |
+
df_falhas,
|
| 1085 |
+
session.geo_col_cdlog,
|
| 1086 |
+
session.geo_col_num,
|
| 1087 |
+
auto_200=auto_200,
|
| 1088 |
+
)
|
| 1089 |
+
except Exception as exc:
|
| 1090 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 1091 |
+
|
| 1092 |
+
session.df_original = df_resultado
|
| 1093 |
+
session.df_filtrado = df_resultado.drop(index=session.outliers_anteriores, errors="ignore")
|
| 1094 |
+
session.geo_falhas_df = df_falhas_novas
|
| 1095 |
+
|
| 1096 |
+
status_html = geocodificacao.formatar_status_geocodificacao(df_resultado, df_falhas_novas, ajustados, manuais=manuais)
|
| 1097 |
+
falhas_html, df_correcoes = geocodificacao.preparar_display_falhas(df_falhas_novas)
|
| 1098 |
+
|
| 1099 |
+
return {
|
| 1100 |
+
"status_html": status_html,
|
| 1101 |
+
"falhas_html": falhas_html,
|
| 1102 |
+
"falhas_para_correcao": dataframe_to_payload(df_correcoes, decimals=None),
|
| 1103 |
+
"mapa_html": charts.criar_mapa(session.df_filtrado),
|
| 1104 |
+
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 1105 |
+
"coords": _build_coords_payload(session.df_original, True),
|
| 1106 |
+
}
|
| 1107 |
+
|
| 1108 |
+
|
| 1109 |
+
def limpar_historico_outliers(session: SessionState) -> dict[str, Any]:
|
| 1110 |
+
if session.df_original is None:
|
| 1111 |
+
raise HTTPException(status_code=400, detail="Carregue dados primeiro")
|
| 1112 |
+
|
| 1113 |
+
session.outliers_anteriores = []
|
| 1114 |
+
session.iteracao = 1
|
| 1115 |
+
session.df_filtrado = session.df_original.copy()
|
| 1116 |
+
session.reset_modelo()
|
| 1117 |
+
|
| 1118 |
+
return {
|
| 1119 |
+
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 1120 |
+
"mapa_html": charts.criar_mapa(session.df_filtrado),
|
| 1121 |
+
"outliers_html": "",
|
| 1122 |
+
"resumo_outliers": "Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0",
|
| 1123 |
+
"contexto": _selection_context(session),
|
| 1124 |
+
}
|
backend/app/services/serializers.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def sanitize_value(value: Any) -> Any:
|
| 12 |
+
if isinstance(value, np.ndarray):
|
| 13 |
+
return [sanitize_value(item) for item in value.tolist()]
|
| 14 |
+
if isinstance(value, np.generic):
|
| 15 |
+
return sanitize_value(value.item())
|
| 16 |
+
if isinstance(value, (np.floating, float)):
|
| 17 |
+
if math.isnan(value) or math.isinf(value):
|
| 18 |
+
return None
|
| 19 |
+
return float(value)
|
| 20 |
+
if isinstance(value, (np.integer, int)):
|
| 21 |
+
return int(value)
|
| 22 |
+
if isinstance(value, (np.bool_, bool)):
|
| 23 |
+
return bool(value)
|
| 24 |
+
if isinstance(value, pd.Timestamp):
|
| 25 |
+
return value.isoformat()
|
| 26 |
+
if isinstance(value, pd.Timedelta):
|
| 27 |
+
return str(value)
|
| 28 |
+
if isinstance(value, pd.Series):
|
| 29 |
+
return [sanitize_value(item) for item in value.tolist()]
|
| 30 |
+
if isinstance(value, pd.Index):
|
| 31 |
+
return [sanitize_value(item) for item in value.tolist()]
|
| 32 |
+
if isinstance(value, pd.DataFrame):
|
| 33 |
+
return sanitize_value(value.to_dict(orient="records"))
|
| 34 |
+
if isinstance(value, Path):
|
| 35 |
+
return str(value)
|
| 36 |
+
if value is None:
|
| 37 |
+
return None
|
| 38 |
+
if isinstance(value, str):
|
| 39 |
+
return value
|
| 40 |
+
if isinstance(value, dict):
|
| 41 |
+
return {str(k): sanitize_value(v) for k, v in value.items()}
|
| 42 |
+
if isinstance(value, (list, tuple, set)):
|
| 43 |
+
return [sanitize_value(v) for v in value]
|
| 44 |
+
try:
|
| 45 |
+
if pd.isna(value):
|
| 46 |
+
return None
|
| 47 |
+
except Exception:
|
| 48 |
+
pass
|
| 49 |
+
return value
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def dataframe_to_payload(df: pd.DataFrame | None, decimals: int | None = None, max_rows: int = 5000) -> dict[str, Any] | None:
|
| 53 |
+
if df is None:
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
df_work = df.copy()
|
| 57 |
+
if decimals is not None:
|
| 58 |
+
numeric_cols = df_work.select_dtypes(include=[np.number]).columns
|
| 59 |
+
df_work[numeric_cols] = df_work[numeric_cols].round(decimals)
|
| 60 |
+
|
| 61 |
+
total_rows = len(df_work)
|
| 62 |
+
truncated = total_rows > max_rows
|
| 63 |
+
if truncated:
|
| 64 |
+
df_work = df_work.head(max_rows)
|
| 65 |
+
|
| 66 |
+
rows: list[dict[str, Any]] = []
|
| 67 |
+
for idx, row in df_work.iterrows():
|
| 68 |
+
payload_row = {"_index": sanitize_value(idx)}
|
| 69 |
+
for col, value in row.items():
|
| 70 |
+
payload_row[str(col)] = sanitize_value(value)
|
| 71 |
+
rows.append(payload_row)
|
| 72 |
+
|
| 73 |
+
columns = ["_index"] + [str(c) for c in df_work.columns]
|
| 74 |
+
return {
|
| 75 |
+
"columns": columns,
|
| 76 |
+
"rows": rows,
|
| 77 |
+
"total_rows": total_rows,
|
| 78 |
+
"returned_rows": len(rows),
|
| 79 |
+
"truncated": truncated,
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def figure_to_payload(fig: Any) -> dict[str, Any] | None:
|
| 84 |
+
if fig is None:
|
| 85 |
+
return None
|
| 86 |
+
try:
|
| 87 |
+
return sanitize_value(fig.to_plotly_json())
|
| 88 |
+
except Exception:
|
| 89 |
+
return None
|
backend/app/services/session_store.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import shutil
|
| 4 |
+
import tempfile
|
| 5 |
+
import uuid
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from fastapi import HTTPException
|
| 9 |
+
|
| 10 |
+
from app.models.session import SessionState
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SessionStore:
|
| 14 |
+
def __init__(self) -> None:
|
| 15 |
+
self._sessions: dict[str, SessionState] = {}
|
| 16 |
+
|
| 17 |
+
def create(self) -> SessionState:
|
| 18 |
+
session_id = uuid.uuid4().hex
|
| 19 |
+
workdir = Path(tempfile.mkdtemp(prefix=f"mesa_{session_id[:8]}_"))
|
| 20 |
+
state = SessionState(session_id=session_id, workdir=workdir)
|
| 21 |
+
self._sessions[session_id] = state
|
| 22 |
+
return state
|
| 23 |
+
|
| 24 |
+
def get(self, session_id: str) -> SessionState:
|
| 25 |
+
state = self._sessions.get(session_id)
|
| 26 |
+
if state is None:
|
| 27 |
+
raise HTTPException(status_code=404, detail="Sessao nao encontrada")
|
| 28 |
+
return state
|
| 29 |
+
|
| 30 |
+
def delete(self, session_id: str) -> None:
|
| 31 |
+
state = self._sessions.pop(session_id, None)
|
| 32 |
+
if state is None:
|
| 33 |
+
return
|
| 34 |
+
shutil.rmtree(state.workdir, ignore_errors=True)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
session_store = SessionStore()
|
backend/app/services/visualizacao_service.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from fastapi import HTTPException
|
| 9 |
+
from joblib import load
|
| 10 |
+
|
| 11 |
+
from app.core.visualizacao import app as viz_app
|
| 12 |
+
from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, avaliar_imovel, exportar_avaliacoes_excel
|
| 13 |
+
from app.core.elaboracao.formatadores import formatar_avaliacao_html
|
| 14 |
+
from app.models.session import SessionState
|
| 15 |
+
from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def carregar_modelo(session: SessionState, caminho_arquivo: str) -> dict[str, Any]:
|
| 22 |
+
try:
|
| 23 |
+
pacote = load(caminho_arquivo)
|
| 24 |
+
except Exception as exc:
|
| 25 |
+
raise HTTPException(status_code=400, detail=f"Falha ao ler arquivo .dai: {exc}") from exc
|
| 26 |
+
|
| 27 |
+
if not isinstance(pacote, dict):
|
| 28 |
+
raise HTTPException(status_code=400, detail="Arquivo .dai invalido")
|
| 29 |
+
|
| 30 |
+
if "versao" not in pacote:
|
| 31 |
+
pacote = _migrar_pacote_v1_para_v2(pacote)
|
| 32 |
+
|
| 33 |
+
faltantes = [k for k in CHAVES_ESPERADAS if k not in pacote]
|
| 34 |
+
if faltantes:
|
| 35 |
+
raise HTTPException(status_code=400, detail=f"Pacote incompleto: {faltantes}")
|
| 36 |
+
|
| 37 |
+
session.pacote_visualizacao = pacote
|
| 38 |
+
session.dados_visualizacao = None
|
| 39 |
+
session.avaliacoes_visualizacao = []
|
| 40 |
+
|
| 41 |
+
status = f"Modelo carregado: {os.path.basename(caminho_arquivo)}"
|
| 42 |
+
badge_html = viz_app._formatar_badge_completo(pacote)
|
| 43 |
+
|
| 44 |
+
return {
|
| 45 |
+
"status": status,
|
| 46 |
+
"badge_html": badge_html,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _tabela_estatisticas(pacote: dict[str, Any]) -> pd.DataFrame:
|
| 51 |
+
estat = pd.DataFrame(pacote["dados"]["estatisticas"])
|
| 52 |
+
if not isinstance(estat.index, pd.RangeIndex):
|
| 53 |
+
estat.insert(0, "Variável", estat.index.astype(str))
|
| 54 |
+
estat = estat.reset_index(drop=True)
|
| 55 |
+
return estat
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _extrair_modelo_info(pacote: dict[str, Any]) -> dict[str, Any]:
|
| 59 |
+
info_transf = pacote["transformacoes"]["info"]
|
| 60 |
+
nome_y, transf_y = info_transf[0].split(": ", 1)
|
| 61 |
+
transformacao_y = transf_y.strip().replace("(y)", "(x)")
|
| 62 |
+
|
| 63 |
+
colunas_x: list[str] = []
|
| 64 |
+
transformacoes_x: dict[str, str] = {}
|
| 65 |
+
for item in info_transf[1:]:
|
| 66 |
+
nome_x, transf_x = item.split(": ", 1)
|
| 67 |
+
nome_x = nome_x.strip()
|
| 68 |
+
colunas_x.append(nome_x)
|
| 69 |
+
transformacoes_x[nome_x] = transf_x.strip()
|
| 70 |
+
|
| 71 |
+
dicotomicas = [str(v) for v in (pacote["transformacoes"].get("dicotomicas", []) or [])]
|
| 72 |
+
codigo_alocado = [str(v) for v in (pacote["transformacoes"].get("codigo_alocado", []) or [])]
|
| 73 |
+
percentuais = [str(v) for v in (pacote["transformacoes"].get("percentuais", []) or [])]
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
"nome_y": nome_y.strip(),
|
| 77 |
+
"transformacao_y": transformacao_y,
|
| 78 |
+
"colunas_x": colunas_x,
|
| 79 |
+
"transformacoes_x": transformacoes_x,
|
| 80 |
+
"dicotomicas": dicotomicas,
|
| 81 |
+
"codigo_alocado": codigo_alocado,
|
| 82 |
+
"percentuais": percentuais,
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def exibir_modelo(session: SessionState) -> dict[str, Any]:
|
| 87 |
+
pacote = session.pacote_visualizacao
|
| 88 |
+
if pacote is None:
|
| 89 |
+
raise HTTPException(status_code=400, detail="Carregue um modelo primeiro")
|
| 90 |
+
|
| 91 |
+
dados = pacote["dados"]["df"].reset_index()
|
| 92 |
+
for col in dados.columns:
|
| 93 |
+
if str(col).lower() in ["lat", "lon"]:
|
| 94 |
+
dados[col] = dados[col].round(6)
|
| 95 |
+
elif pd.api.types.is_numeric_dtype(dados[col]):
|
| 96 |
+
dados[col] = dados[col].round(2)
|
| 97 |
+
|
| 98 |
+
estat = _tabela_estatisticas(pacote).round(2)
|
| 99 |
+
|
| 100 |
+
escalas_html = viz_app.formatar_escalas_html(pacote["transformacoes"]["info"])
|
| 101 |
+
|
| 102 |
+
X = pacote["transformacoes"]["X"].reset_index()
|
| 103 |
+
y = pacote["transformacoes"]["y"].reset_index()
|
| 104 |
+
if "index" in y.columns and "index" in X.columns:
|
| 105 |
+
y = y.drop(columns=["index"])
|
| 106 |
+
df_xy = pd.concat([X, y], axis=1)
|
| 107 |
+
df_xy = df_xy.loc[:, ~df_xy.columns.duplicated()].round(2)
|
| 108 |
+
|
| 109 |
+
resumo_html = viz_app.formatar_resumo_html(viz_app.reorganizar_modelos_resumos(pacote["modelo"]["diagnosticos"]))
|
| 110 |
+
|
| 111 |
+
tab_coef = pd.DataFrame(pacote["modelo"]["coeficientes"])
|
| 112 |
+
if not isinstance(tab_coef.index, pd.RangeIndex):
|
| 113 |
+
tab_coef.insert(0, "Variável", tab_coef.index.astype(str))
|
| 114 |
+
tab_coef = tab_coef.reset_index(drop=True)
|
| 115 |
+
mask = tab_coef["Variável"].astype(str).str.lower().isin(["intercept", "const", "(intercept)"])
|
| 116 |
+
if mask.any():
|
| 117 |
+
tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
|
| 118 |
+
tab_coef = tab_coef.round(2)
|
| 119 |
+
|
| 120 |
+
tab_obs_calc = pacote["modelo"]["obs_calc"].reset_index().round(2)
|
| 121 |
+
|
| 122 |
+
figs = viz_app.gerar_todos_graficos(pacote)
|
| 123 |
+
|
| 124 |
+
info = _extrair_modelo_info(pacote)
|
| 125 |
+
mapa_html = viz_app.criar_mapa(dados, col_y=info["nome_y"])
|
| 126 |
+
|
| 127 |
+
colunas_numericas = [
|
| 128 |
+
str(col)
|
| 129 |
+
for col in dados.select_dtypes(include=[np.number]).columns
|
| 130 |
+
if str(col).lower() not in ["lat", "lon", "latitude", "longitude", "index"]
|
| 131 |
+
]
|
| 132 |
+
choices_mapa = ["Visualização Padrão"] + colunas_numericas
|
| 133 |
+
|
| 134 |
+
session.dados_visualizacao = dados
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"dados": dataframe_to_payload(dados, decimals=2),
|
| 138 |
+
"estatisticas": dataframe_to_payload(estat, decimals=2),
|
| 139 |
+
"escalas_html": escalas_html,
|
| 140 |
+
"dados_transformados": dataframe_to_payload(df_xy, decimals=2),
|
| 141 |
+
"resumo_html": resumo_html,
|
| 142 |
+
"coeficientes": dataframe_to_payload(tab_coef, decimals=2),
|
| 143 |
+
"obs_calc": dataframe_to_payload(tab_obs_calc, decimals=2),
|
| 144 |
+
"grafico_obs_calc": figure_to_payload(figs.get("obs_calc")),
|
| 145 |
+
"grafico_residuos": figure_to_payload(figs.get("residuos")),
|
| 146 |
+
"grafico_histograma": figure_to_payload(figs.get("hist")),
|
| 147 |
+
"grafico_cook": figure_to_payload(figs.get("cook")),
|
| 148 |
+
"grafico_correlacao": figure_to_payload(figs.get("corr")),
|
| 149 |
+
"mapa_html": mapa_html,
|
| 150 |
+
"mapa_choices": choices_mapa,
|
| 151 |
+
"campos_avaliacao": campos_avaliacao(session),
|
| 152 |
+
"meta_modelo": sanitize_value(info),
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def atualizar_mapa(session: SessionState, variavel_mapa: str | None) -> dict[str, Any]:
|
| 157 |
+
pacote = session.pacote_visualizacao
|
| 158 |
+
dados = session.dados_visualizacao
|
| 159 |
+
if pacote is None or dados is None or dados.empty:
|
| 160 |
+
raise HTTPException(status_code=400, detail="Exiba o modelo antes de atualizar o mapa")
|
| 161 |
+
|
| 162 |
+
info = _extrair_modelo_info(pacote)
|
| 163 |
+
tamanho_col = None
|
| 164 |
+
if variavel_mapa and variavel_mapa != "Visualização Padrão":
|
| 165 |
+
tamanho_col = variavel_mapa
|
| 166 |
+
|
| 167 |
+
mapa_html = viz_app.criar_mapa(dados, tamanho_col=tamanho_col, col_y=info["nome_y"])
|
| 168 |
+
return {"mapa_html": mapa_html}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def campos_avaliacao(session: SessionState) -> list[dict[str, Any]]:
|
| 172 |
+
pacote = session.pacote_visualizacao
|
| 173 |
+
if pacote is None:
|
| 174 |
+
return []
|
| 175 |
+
|
| 176 |
+
info = _extrair_modelo_info(pacote)
|
| 177 |
+
colunas_x = info["colunas_x"]
|
| 178 |
+
dicotomicas = info["dicotomicas"]
|
| 179 |
+
codigo_alocado = info["codigo_alocado"]
|
| 180 |
+
percentuais = info["percentuais"]
|
| 181 |
+
|
| 182 |
+
estat = pacote["dados"]["estatisticas"]
|
| 183 |
+
if isinstance(estat, pd.DataFrame):
|
| 184 |
+
estat_df = estat.copy()
|
| 185 |
+
else:
|
| 186 |
+
estat_df = pd.DataFrame(estat)
|
| 187 |
+
|
| 188 |
+
if "Variável" in estat_df.columns:
|
| 189 |
+
est_idx = estat_df.set_index("Variável")
|
| 190 |
+
elif not isinstance(estat_df.index, pd.RangeIndex):
|
| 191 |
+
est_idx = estat_df
|
| 192 |
+
else:
|
| 193 |
+
est_idx = pd.DataFrame()
|
| 194 |
+
|
| 195 |
+
if not est_idx.empty:
|
| 196 |
+
est_idx.index = est_idx.index.map(str)
|
| 197 |
+
|
| 198 |
+
campos: list[dict[str, Any]] = []
|
| 199 |
+
for col in colunas_x:
|
| 200 |
+
placeholder = ""
|
| 201 |
+
if col in dicotomicas:
|
| 202 |
+
placeholder = "0 ou 1"
|
| 203 |
+
tipo = "dicotomica"
|
| 204 |
+
elif col in codigo_alocado and col in est_idx.index:
|
| 205 |
+
min_v = est_idx.loc[col, "Mínimo"]
|
| 206 |
+
max_v = est_idx.loc[col, "Máximo"]
|
| 207 |
+
placeholder = f"cod. {int(min_v)} a {int(max_v)}"
|
| 208 |
+
tipo = "codigo_alocado"
|
| 209 |
+
elif col in percentuais:
|
| 210 |
+
placeholder = "0 a 1"
|
| 211 |
+
tipo = "percentual"
|
| 212 |
+
elif col in est_idx.index:
|
| 213 |
+
min_v = est_idx.loc[col, "Mínimo"]
|
| 214 |
+
max_v = est_idx.loc[col, "Máximo"]
|
| 215 |
+
placeholder = f"{min_v} — {max_v}"
|
| 216 |
+
tipo = "numerica"
|
| 217 |
+
else:
|
| 218 |
+
tipo = "numerica"
|
| 219 |
+
|
| 220 |
+
campos.append({"coluna": col, "placeholder": placeholder, "tipo": tipo})
|
| 221 |
+
|
| 222 |
+
return sanitize_value(campos)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def calcular_avaliacao(session: SessionState, valores_x: dict[str, Any], indice_base: str | None) -> dict[str, Any]:
|
| 226 |
+
pacote = session.pacote_visualizacao
|
| 227 |
+
if pacote is None:
|
| 228 |
+
raise HTTPException(status_code=400, detail="Carregue um modelo primeiro")
|
| 229 |
+
|
| 230 |
+
info = _extrair_modelo_info(pacote)
|
| 231 |
+
colunas_x = info["colunas_x"]
|
| 232 |
+
|
| 233 |
+
entradas: dict[str, float] = {}
|
| 234 |
+
for col in colunas_x:
|
| 235 |
+
if col not in valores_x:
|
| 236 |
+
raise HTTPException(status_code=400, detail=f"Valor ausente para {col}")
|
| 237 |
+
try:
|
| 238 |
+
entradas[col] = float(valores_x[col])
|
| 239 |
+
except Exception as exc:
|
| 240 |
+
raise HTTPException(status_code=400, detail=f"Valor invalido para {col}") from exc
|
| 241 |
+
|
| 242 |
+
estatisticas_df = pacote["dados"]["estatisticas"]
|
| 243 |
+
if isinstance(estatisticas_df, pd.DataFrame):
|
| 244 |
+
est_df = estatisticas_df
|
| 245 |
+
else:
|
| 246 |
+
est_df = pd.DataFrame(estatisticas_df)
|
| 247 |
+
|
| 248 |
+
if "Variável" in est_df.columns:
|
| 249 |
+
est_idx = est_df.set_index("Variável")
|
| 250 |
+
else:
|
| 251 |
+
est_idx = est_df
|
| 252 |
+
|
| 253 |
+
for col in colunas_x:
|
| 254 |
+
valor = entradas[col]
|
| 255 |
+
if col in info["dicotomicas"] and valor not in (0, 0.0, 1, 1.0):
|
| 256 |
+
raise HTTPException(status_code=400, detail=f"{col} aceita apenas 0 ou 1")
|
| 257 |
+
if col in info["codigo_alocado"] and col in est_idx.index:
|
| 258 |
+
min_v = float(est_idx.loc[col, "Mínimo"])
|
| 259 |
+
max_v = float(est_idx.loc[col, "Máximo"])
|
| 260 |
+
if float(valor) != int(float(valor)) or valor < min_v or valor > max_v:
|
| 261 |
+
raise HTTPException(status_code=400, detail=f"{col} aceita inteiros de {int(min_v)} a {int(max_v)}")
|
| 262 |
+
if col in info["percentuais"] and (valor < 0 or valor > 1):
|
| 263 |
+
raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
|
| 264 |
+
|
| 265 |
+
resultado = avaliar_imovel(
|
| 266 |
+
modelo_sm=pacote["modelo"]["sm"],
|
| 267 |
+
valores_x=entradas,
|
| 268 |
+
colunas_x=colunas_x,
|
| 269 |
+
transformacoes_x=info["transformacoes_x"],
|
| 270 |
+
transformacao_y=info["transformacao_y"],
|
| 271 |
+
estatisticas_df=est_df,
|
| 272 |
+
dicotomicas=info["dicotomicas"],
|
| 273 |
+
codigo_alocado=info["codigo_alocado"],
|
| 274 |
+
percentuais=info["percentuais"],
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
if resultado is None:
|
| 278 |
+
raise HTTPException(status_code=400, detail="Erro ao calcular avaliacao")
|
| 279 |
+
|
| 280 |
+
session.avaliacoes_visualizacao.append(resultado)
|
| 281 |
+
|
| 282 |
+
indice = int(indice_base) - 1 if indice_base else 0
|
| 283 |
+
html = formatar_avaliacao_html(session.avaliacoes_visualizacao, indice_base=indice, elem_id_excluir="excluir-aval-viz")
|
| 284 |
+
choices = [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))]
|
| 285 |
+
base_value = indice_base if indice_base else "1"
|
| 286 |
+
|
| 287 |
+
return {
|
| 288 |
+
"resultado_html": html,
|
| 289 |
+
"avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
|
| 290 |
+
"base_choices": choices,
|
| 291 |
+
"base_value": base_value,
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def limpar_avaliacoes(session: SessionState) -> dict[str, Any]:
|
| 296 |
+
session.avaliacoes_visualizacao = []
|
| 297 |
+
return {
|
| 298 |
+
"resultado_html": "",
|
| 299 |
+
"avaliacoes": [],
|
| 300 |
+
"base_choices": [],
|
| 301 |
+
"base_value": None,
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def excluir_avaliacao(session: SessionState, indice_str: str | None, indice_base_str: str | None) -> dict[str, Any]:
|
| 306 |
+
if not indice_str or not session.avaliacoes_visualizacao:
|
| 307 |
+
return {
|
| 308 |
+
"resultado_html": None,
|
| 309 |
+
"avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
|
| 310 |
+
"base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
|
| 311 |
+
"base_value": indice_base_str,
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
try:
|
| 315 |
+
idx = int(indice_str.strip()) - 1
|
| 316 |
+
except Exception:
|
| 317 |
+
return {
|
| 318 |
+
"resultado_html": None,
|
| 319 |
+
"avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
|
| 320 |
+
"base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
|
| 321 |
+
"base_value": indice_base_str,
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
if idx < 0 or idx >= len(session.avaliacoes_visualizacao):
|
| 325 |
+
return {
|
| 326 |
+
"resultado_html": None,
|
| 327 |
+
"avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
|
| 328 |
+
"base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
|
| 329 |
+
"base_value": indice_base_str,
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
nova_lista = [a for i, a in enumerate(session.avaliacoes_visualizacao) if i != idx]
|
| 333 |
+
session.avaliacoes_visualizacao = nova_lista
|
| 334 |
+
|
| 335 |
+
if not nova_lista:
|
| 336 |
+
return {
|
| 337 |
+
"resultado_html": "",
|
| 338 |
+
"avaliacoes": [],
|
| 339 |
+
"base_choices": [],
|
| 340 |
+
"base_value": None,
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 344 |
+
if base >= len(nova_lista):
|
| 345 |
+
base = len(nova_lista) - 1
|
| 346 |
+
if base < 0:
|
| 347 |
+
base = 0
|
| 348 |
+
|
| 349 |
+
html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-viz")
|
| 350 |
+
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 351 |
+
|
| 352 |
+
return {
|
| 353 |
+
"resultado_html": html,
|
| 354 |
+
"avaliacoes": sanitize_value(nova_lista),
|
| 355 |
+
"base_choices": choices,
|
| 356 |
+
"base_value": str(base + 1),
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def atualizar_base(session: SessionState, indice_base_str: str | None) -> dict[str, Any]:
|
| 361 |
+
if not session.avaliacoes_visualizacao:
|
| 362 |
+
return {"resultado_html": ""}
|
| 363 |
+
indice = int(indice_base_str) - 1 if indice_base_str else 0
|
| 364 |
+
html = formatar_avaliacao_html(session.avaliacoes_visualizacao, indice_base=indice, elem_id_excluir="excluir-aval-viz")
|
| 365 |
+
return {"resultado_html": html}
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def exportar_avaliacoes(session: SessionState) -> str:
|
| 369 |
+
caminho = exportar_avaliacoes_excel(session.avaliacoes_visualizacao)
|
| 370 |
+
if not caminho:
|
| 371 |
+
raise HTTPException(status_code=400, detail="Sem avaliacoes para exportar")
|
| 372 |
+
return caminho
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def limpar_tudo_visualizacao(session: SessionState) -> dict[str, Any]:
|
| 376 |
+
session.reset_visualizacao()
|
| 377 |
+
return {
|
| 378 |
+
"status": "",
|
| 379 |
+
"badge_html": "",
|
| 380 |
+
}
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.111.0
|
| 2 |
+
uvicorn[standard]>=0.30.0
|
| 3 |
+
python-multipart>=0.0.9
|
| 4 |
+
pydantic>=2.7.0
|
| 5 |
+
pandas
|
| 6 |
+
numpy
|
| 7 |
+
statsmodels
|
| 8 |
+
scipy
|
| 9 |
+
plotly
|
| 10 |
+
folium
|
| 11 |
+
branca
|
| 12 |
+
joblib
|
| 13 |
+
openpyxl
|
| 14 |
+
geopandas
|
| 15 |
+
fiona
|
| 16 |
+
gradio>=4.0
|
backend/run_backend.sh
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
frontend/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.0" />
|
| 6 |
+
<title>MESA Frame</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "mesa-frame-frontend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"plotly.js-dist-min": "^2.35.2",
|
| 13 |
+
"react": "^18.3.1",
|
| 14 |
+
"react-dom": "^18.3.1",
|
| 15 |
+
"react-plotly.js": "^2.6.0"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@vitejs/plugin-react": "^4.3.1",
|
| 19 |
+
"vite": "^5.4.8"
|
| 20 |
+
}
|
| 21 |
+
}
|
frontend/public/logo_mesa.png
ADDED
|
Git LFS Details
|
frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react'
|
| 2 |
+
import { api } from './api'
|
| 3 |
+
import ElaboracaoTab from './components/ElaboracaoTab'
|
| 4 |
+
import VisualizacaoTab from './components/VisualizacaoTab'
|
| 5 |
+
|
| 6 |
+
const TABS = [
|
| 7 |
+
{ key: 'Pesquisa', label: 'Pesquisa', hint: 'Módulo em desenvolvimento' },
|
| 8 |
+
{ key: 'Elaboração/Edição', label: 'Elaboração/Edição', hint: 'Fluxo completo de modelagem' },
|
| 9 |
+
{ key: 'Visualização/Avaliação', label: 'Visualização/Avaliação', hint: 'Leitura e avaliação de modelos .dai' },
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
export default function App() {
|
| 13 |
+
const [activeTab, setActiveTab] = useState(TABS[0].key)
|
| 14 |
+
const [sessionId, setSessionId] = useState('')
|
| 15 |
+
const [bootError, setBootError] = useState('')
|
| 16 |
+
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
let mounted = true
|
| 19 |
+
|
| 20 |
+
api.createSession()
|
| 21 |
+
.then((resp) => {
|
| 22 |
+
if (mounted) setSessionId(resp.session_id)
|
| 23 |
+
})
|
| 24 |
+
.catch((err) => {
|
| 25 |
+
if (mounted) setBootError(err.message)
|
| 26 |
+
})
|
| 27 |
+
|
| 28 |
+
return () => {
|
| 29 |
+
mounted = false
|
| 30 |
+
}
|
| 31 |
+
}, [])
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<div className="app-shell">
|
| 35 |
+
<header className="app-header app-header-logo-only">
|
| 36 |
+
<div className="brand-mark" aria-hidden="true">
|
| 37 |
+
<img src="/logo_mesa.png" alt="MESA" />
|
| 38 |
+
</div>
|
| 39 |
+
</header>
|
| 40 |
+
|
| 41 |
+
<nav className="tabs" aria-label="Navegação principal">
|
| 42 |
+
{TABS.map((tab) => {
|
| 43 |
+
const active = tab.key === activeTab
|
| 44 |
+
return (
|
| 45 |
+
<button
|
| 46 |
+
key={tab.key}
|
| 47 |
+
className={active ? 'tab-pill active' : 'tab-pill'}
|
| 48 |
+
onClick={() => setActiveTab(tab.key)}
|
| 49 |
+
type="button"
|
| 50 |
+
>
|
| 51 |
+
<strong>{tab.label}</strong>
|
| 52 |
+
<small>{tab.hint}</small>
|
| 53 |
+
</button>
|
| 54 |
+
)
|
| 55 |
+
})}
|
| 56 |
+
</nav>
|
| 57 |
+
|
| 58 |
+
{bootError ? <div className="error-line">Falha ao criar sessão: {bootError}</div> : null}
|
| 59 |
+
|
| 60 |
+
{activeTab === 'Pesquisa' ? (
|
| 61 |
+
<section className="workflow-section placeholder-section" style={{ '--section-order': 1 }}>
|
| 62 |
+
<header className="section-head">
|
| 63 |
+
<span className="section-index">1</span>
|
| 64 |
+
<div className="section-title-wrap">
|
| 65 |
+
<h3>Pesquisa</h3>
|
| 66 |
+
<p>Este módulo segue em desenvolvimento no app original e foi mantido com o mesmo status.</p>
|
| 67 |
+
</div>
|
| 68 |
+
</header>
|
| 69 |
+
<div className="section-body">
|
| 70 |
+
<div className="empty-box">Aba disponível para expansão futura.</div>
|
| 71 |
+
</div>
|
| 72 |
+
</section>
|
| 73 |
+
) : null}
|
| 74 |
+
|
| 75 |
+
{activeTab === 'Elaboração/Edição' ? <ElaboracaoTab sessionId={sessionId} /> : null}
|
| 76 |
+
{activeTab === 'Visualização/Avaliação' ? <VisualizacaoTab sessionId={sessionId} /> : null}
|
| 77 |
+
</div>
|
| 78 |
+
)
|
| 79 |
+
}
|
frontend/src/api.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000'
|
| 2 |
+
|
| 3 |
+
async function handleResponse(response) {
|
| 4 |
+
if (!response.ok) {
|
| 5 |
+
let detail = 'Erro inesperado'
|
| 6 |
+
try {
|
| 7 |
+
const data = await response.json()
|
| 8 |
+
detail = data.detail || detail
|
| 9 |
+
} catch {
|
| 10 |
+
detail = response.statusText || detail
|
| 11 |
+
}
|
| 12 |
+
throw new Error(detail)
|
| 13 |
+
}
|
| 14 |
+
return response
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
async function postJson(path, body) {
|
| 18 |
+
const response = await fetch(`${API_BASE}${path}`, {
|
| 19 |
+
method: 'POST',
|
| 20 |
+
headers: { 'Content-Type': 'application/json' },
|
| 21 |
+
body: JSON.stringify(body),
|
| 22 |
+
})
|
| 23 |
+
await handleResponse(response)
|
| 24 |
+
return response.json()
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async function postForm(path, formData) {
|
| 28 |
+
const response = await fetch(`${API_BASE}${path}`, {
|
| 29 |
+
method: 'POST',
|
| 30 |
+
body: formData,
|
| 31 |
+
})
|
| 32 |
+
await handleResponse(response)
|
| 33 |
+
return response.json()
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function getJson(path) {
|
| 37 |
+
const response = await fetch(`${API_BASE}${path}`)
|
| 38 |
+
await handleResponse(response)
|
| 39 |
+
return response.json()
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
async function getBlob(path) {
|
| 43 |
+
const response = await fetch(`${API_BASE}${path}`)
|
| 44 |
+
await handleResponse(response)
|
| 45 |
+
return response.blob()
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export function downloadBlob(blob, fileName) {
|
| 49 |
+
const url = URL.createObjectURL(blob)
|
| 50 |
+
const a = document.createElement('a')
|
| 51 |
+
a.href = url
|
| 52 |
+
a.download = fileName
|
| 53 |
+
document.body.appendChild(a)
|
| 54 |
+
a.click()
|
| 55 |
+
a.remove()
|
| 56 |
+
URL.revokeObjectURL(url)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export const api = {
|
| 60 |
+
createSession: () => postJson('/api/sessions', {}),
|
| 61 |
+
|
| 62 |
+
uploadElaboracaoFile(sessionId, file) {
|
| 63 |
+
const form = new FormData()
|
| 64 |
+
form.append('session_id', sessionId)
|
| 65 |
+
form.append('file', file)
|
| 66 |
+
return postForm('/api/elaboracao/upload', form)
|
| 67 |
+
},
|
| 68 |
+
|
| 69 |
+
confirmSheet: (sessionId, sheetName) => postJson('/api/elaboracao/confirm-sheet', { session_id: sessionId, sheet_name: sheetName }),
|
| 70 |
+
mapCoords: (sessionId, colLat, colLon) => postJson('/api/elaboracao/map-coords', { session_id: sessionId, col_lat: colLat, col_lon: colLon }),
|
| 71 |
+
geocodificar: (sessionId, colCdlog, colNum, auto200) => postJson('/api/elaboracao/geocodificar', { session_id: sessionId, col_cdlog: colCdlog, col_num: colNum, auto_200: auto200 }),
|
| 72 |
+
geocodificarCorrecoes: (sessionId, correcoes, auto200) => postJson('/api/elaboracao/geocodificar-correcoes', { session_id: sessionId, correcoes, auto_200: auto200 }),
|
| 73 |
+
|
| 74 |
+
applySelection(payload) {
|
| 75 |
+
return postJson('/api/elaboracao/apply-selection', payload)
|
| 76 |
+
},
|
| 77 |
+
classifyElaboracaoX: (sessionId, colunasX) => postJson('/api/elaboracao/classify-x', { session_id: sessionId, colunas_x: colunasX }),
|
| 78 |
+
|
| 79 |
+
searchTransformations: (sessionId, grauCoef, grauF) => postJson('/api/elaboracao/search-transformations', {
|
| 80 |
+
session_id: sessionId,
|
| 81 |
+
grau_min_coef: grauCoef,
|
| 82 |
+
grau_min_f: grauF,
|
| 83 |
+
}),
|
| 84 |
+
|
| 85 |
+
adoptSuggestion: (sessionId, indice) => postJson('/api/elaboracao/adopt-suggestion', { session_id: sessionId, indice }),
|
| 86 |
+
|
| 87 |
+
fitModel(payload) {
|
| 88 |
+
return postJson('/api/elaboracao/fit-model', payload)
|
| 89 |
+
},
|
| 90 |
+
|
| 91 |
+
updateModelDispersao: (sessionId, tipo) => postJson('/api/elaboracao/model-dispersao', { session_id: sessionId, tipo }),
|
| 92 |
+
|
| 93 |
+
applyOutlierFilters: (sessionId, filtros) => postJson('/api/elaboracao/outliers/apply-filters', { session_id: sessionId, filtros }),
|
| 94 |
+
restartOutlierIteration: (sessionId, outliersTexto, reincluirTexto, grauCoef, grauF) => postJson('/api/elaboracao/outliers/restart', {
|
| 95 |
+
session_id: sessionId,
|
| 96 |
+
outliers_texto: outliersTexto,
|
| 97 |
+
reincluir_texto: reincluirTexto,
|
| 98 |
+
grau_min_coef: grauCoef,
|
| 99 |
+
grau_min_f: grauF,
|
| 100 |
+
}),
|
| 101 |
+
outlierSummary: (sessionId, outliersTexto, reincluirTexto) => postJson('/api/elaboracao/outliers/summary', {
|
| 102 |
+
session_id: sessionId,
|
| 103 |
+
outliers_texto: outliersTexto,
|
| 104 |
+
reincluir_texto: reincluirTexto,
|
| 105 |
+
}),
|
| 106 |
+
clearOutlierHistory: (sessionId) => postJson('/api/elaboracao/outliers/clear-history', { session_id: sessionId }),
|
| 107 |
+
|
| 108 |
+
evaluationFieldsElab: (sessionId) => postJson('/api/elaboracao/evaluation/fields', { session_id: sessionId }),
|
| 109 |
+
evaluationCalculateElab: (sessionId, valoresX, indiceBase) => postJson('/api/elaboracao/evaluation/calculate', {
|
| 110 |
+
session_id: sessionId,
|
| 111 |
+
valores_x: valoresX,
|
| 112 |
+
indice_base: indiceBase,
|
| 113 |
+
}),
|
| 114 |
+
evaluationClearElab: (sessionId) => postJson('/api/elaboracao/evaluation/clear', { session_id: sessionId }),
|
| 115 |
+
evaluationDeleteElab: (sessionId, indice, indiceBase) => postJson('/api/elaboracao/evaluation/delete', {
|
| 116 |
+
session_id: sessionId,
|
| 117 |
+
indice,
|
| 118 |
+
indice_base: indiceBase,
|
| 119 |
+
}),
|
| 120 |
+
getAvaliadores: () => getJson('/api/elaboracao/avaliadores'),
|
| 121 |
+
evaluationBaseElab: (sessionId, indiceBase) => postJson('/api/elaboracao/evaluation/base', {
|
| 122 |
+
session_id: sessionId,
|
| 123 |
+
indice_base: indiceBase,
|
| 124 |
+
}),
|
| 125 |
+
exportEvaluationElab: async (sessionId) => getBlob(`/api/elaboracao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
|
| 126 |
+
exportModel: async (sessionId, nomeArquivo, elaborador) => {
|
| 127 |
+
const response = await fetch(`${API_BASE}/api/elaboracao/export-model`, {
|
| 128 |
+
method: 'POST',
|
| 129 |
+
headers: { 'Content-Type': 'application/json' },
|
| 130 |
+
body: JSON.stringify({ session_id: sessionId, nome_arquivo: nomeArquivo, elaborador }),
|
| 131 |
+
})
|
| 132 |
+
await handleResponse(response)
|
| 133 |
+
return response.blob()
|
| 134 |
+
},
|
| 135 |
+
exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`),
|
| 136 |
+
updateElaboracaoMap: (sessionId, variavelMapa) => postJson('/api/elaboracao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa }),
|
| 137 |
+
getContext: (sessionId) => getJson(`/api/elaboracao/context?session_id=${encodeURIComponent(sessionId)}`),
|
| 138 |
+
|
| 139 |
+
uploadVisualizacaoFile(sessionId, file) {
|
| 140 |
+
const form = new FormData()
|
| 141 |
+
form.append('session_id', sessionId)
|
| 142 |
+
form.append('file', file)
|
| 143 |
+
return postForm('/api/visualizacao/upload', form)
|
| 144 |
+
},
|
| 145 |
+
exibirVisualizacao: (sessionId) => postJson('/api/visualizacao/exibir', { session_id: sessionId }),
|
| 146 |
+
updateVisualizacaoMap: (sessionId, variavelMapa) => postJson('/api/visualizacao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa }),
|
| 147 |
+
evaluationFieldsViz: (sessionId) => postJson('/api/visualizacao/evaluation/fields', { session_id: sessionId }),
|
| 148 |
+
evaluationCalculateViz: (sessionId, valoresX, indiceBase) => postJson('/api/visualizacao/evaluation/calculate', {
|
| 149 |
+
session_id: sessionId,
|
| 150 |
+
valores_x: valoresX,
|
| 151 |
+
indice_base: indiceBase,
|
| 152 |
+
}),
|
| 153 |
+
evaluationClearViz: (sessionId) => postJson('/api/visualizacao/evaluation/clear', { session_id: sessionId }),
|
| 154 |
+
evaluationDeleteViz: (sessionId, indice, indiceBase) => postJson('/api/visualizacao/evaluation/delete', {
|
| 155 |
+
session_id: sessionId,
|
| 156 |
+
indice,
|
| 157 |
+
indice_base: indiceBase,
|
| 158 |
+
}),
|
| 159 |
+
evaluationBaseViz: (sessionId, indiceBase) => postJson('/api/visualizacao/evaluation/base', {
|
| 160 |
+
session_id: sessionId,
|
| 161 |
+
indice_base: indiceBase,
|
| 162 |
+
}),
|
| 163 |
+
exportEvaluationViz: (sessionId) => getBlob(`/api/visualizacao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
|
| 164 |
+
clearVisualizacao: (sessionId) => postJson('/api/visualizacao/clear', { session_id: sessionId }),
|
| 165 |
+
}
|
frontend/src/components/DataTable.jsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
|
| 3 |
+
export default function DataTable({ table, maxHeight = 320 }) {
|
| 4 |
+
if (!table || !table.columns || !table.rows) {
|
| 5 |
+
return <div className="empty-box">Sem dados.</div>
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<div className="table-wrapper" style={{ maxHeight }}>
|
| 10 |
+
<table>
|
| 11 |
+
<thead>
|
| 12 |
+
<tr>
|
| 13 |
+
{table.columns.map((col) => (
|
| 14 |
+
<th key={col}>{col}</th>
|
| 15 |
+
))}
|
| 16 |
+
</tr>
|
| 17 |
+
</thead>
|
| 18 |
+
<tbody>
|
| 19 |
+
{table.rows.map((row, i) => (
|
| 20 |
+
<tr key={i}>
|
| 21 |
+
{table.columns.map((col) => (
|
| 22 |
+
<td key={`${i}-${col}`}>{String(row[col] ?? '')}</td>
|
| 23 |
+
))}
|
| 24 |
+
</tr>
|
| 25 |
+
))}
|
| 26 |
+
</tbody>
|
| 27 |
+
</table>
|
| 28 |
+
{table.truncated ? (
|
| 29 |
+
<div className="table-hint">Mostrando {table.returned_rows} de {table.total_rows} linhas.</div>
|
| 30 |
+
) : null}
|
| 31 |
+
</div>
|
| 32 |
+
)
|
| 33 |
+
}
|
frontend/src/components/ElaboracaoTab.jsx
ADDED
|
@@ -0,0 +1,1331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
+
import { api, downloadBlob } from '../api'
|
| 3 |
+
import DataTable from './DataTable'
|
| 4 |
+
import MapFrame from './MapFrame'
|
| 5 |
+
import PlotFigure from './PlotFigure'
|
| 6 |
+
import SectionBlock from './SectionBlock'
|
| 7 |
+
|
| 8 |
+
const OPERADORES = ['<=', '>=', '<', '>', '=']
|
| 9 |
+
const GRAUS_COEF = [
|
| 10 |
+
{ value: 0, label: 'Sem enquadramento' },
|
| 11 |
+
{ value: 1, label: 'Grau I (p<=30%)' },
|
| 12 |
+
{ value: 2, label: 'Grau II (p<=20%)' },
|
| 13 |
+
{ value: 3, label: 'Grau III (p<=10%)' },
|
| 14 |
+
]
|
| 15 |
+
const GRAUS_F = [
|
| 16 |
+
{ value: 0, label: 'Sem enquadramento' },
|
| 17 |
+
{ value: 1, label: 'Grau I (alpha=5%)' },
|
| 18 |
+
{ value: 2, label: 'Grau II (alpha=2%)' },
|
| 19 |
+
{ value: 3, label: 'Grau III (alpha=1%)' },
|
| 20 |
+
]
|
| 21 |
+
const GRAU_LABEL_CURTO = {
|
| 22 |
+
0: 'Sem enq.',
|
| 23 |
+
1: 'Grau I',
|
| 24 |
+
2: 'Grau II',
|
| 25 |
+
3: 'Grau III',
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function grauBadgeClass(value) {
|
| 29 |
+
const grau = Number(value)
|
| 30 |
+
if (grau >= 3) return 'grau-badge grau-3'
|
| 31 |
+
if (grau === 2) return 'grau-badge grau-2'
|
| 32 |
+
if (grau === 1) return 'grau-badge grau-1'
|
| 33 |
+
return 'grau-badge grau-0'
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
function defaultFiltros() {
|
| 37 |
+
return [
|
| 38 |
+
{ variavel: 'Resíduo Pad.', operador: '<=', valor: -2 },
|
| 39 |
+
{ variavel: 'Resíduo Pad.', operador: '>=', valor: 2 },
|
| 40 |
+
]
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function joinSelection(values) {
|
| 44 |
+
return values.join(', ')
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function formatConselhoRegistro(elaborador) {
|
| 48 |
+
if (!elaborador) return ''
|
| 49 |
+
const conselho = String(elaborador.conselho || '').trim()
|
| 50 |
+
const estado = String(elaborador.estado_conselho || '').trim()
|
| 51 |
+
const numero = String(elaborador.numero_conselho || '').trim()
|
| 52 |
+
if (conselho && estado && numero) return `${conselho}/${estado} ${numero}`
|
| 53 |
+
return [conselho, numero, estado].filter(Boolean).join(' ')
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function formatTransformacaoBadge(transformacao) {
|
| 57 |
+
const valor = String(transformacao || '').trim()
|
| 58 |
+
if (!valor || valor === '(x)' || valor === '(y)' || valor === 'x' || valor === 'y') return ''
|
| 59 |
+
return valor
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export default function ElaboracaoTab({ sessionId }) {
|
| 63 |
+
const [loading, setLoading] = useState(false)
|
| 64 |
+
const [error, setError] = useState('')
|
| 65 |
+
const [status, setStatus] = useState('')
|
| 66 |
+
|
| 67 |
+
const [uploadedFile, setUploadedFile] = useState(null)
|
| 68 |
+
const [requiresSheet, setRequiresSheet] = useState(false)
|
| 69 |
+
const [sheetOptions, setSheetOptions] = useState([])
|
| 70 |
+
const [selectedSheet, setSelectedSheet] = useState('')
|
| 71 |
+
const [elaborador, setElaborador] = useState(null)
|
| 72 |
+
|
| 73 |
+
const [dados, setDados] = useState(null)
|
| 74 |
+
const [mapaHtml, setMapaHtml] = useState('')
|
| 75 |
+
const [mapaVariavel, setMapaVariavel] = useState('Visualização Padrão')
|
| 76 |
+
|
| 77 |
+
const [coordsInfo, setCoordsInfo] = useState(null)
|
| 78 |
+
const [manualLat, setManualLat] = useState('')
|
| 79 |
+
const [manualLon, setManualLon] = useState('')
|
| 80 |
+
const [geoCdlog, setGeoCdlog] = useState('')
|
| 81 |
+
const [geoNum, setGeoNum] = useState('')
|
| 82 |
+
const [geoAuto200, setGeoAuto200] = useState(false)
|
| 83 |
+
const [geoStatusHtml, setGeoStatusHtml] = useState('')
|
| 84 |
+
const [geoFalhasHtml, setGeoFalhasHtml] = useState('')
|
| 85 |
+
const [geoCorrecoes, setGeoCorrecoes] = useState([])
|
| 86 |
+
const [manualMapError, setManualMapError] = useState('')
|
| 87 |
+
const [geoProcessError, setGeoProcessError] = useState('')
|
| 88 |
+
const [coordsMode, setCoordsMode] = useState('menu')
|
| 89 |
+
|
| 90 |
+
const [colunasNumericas, setColunasNumericas] = useState([])
|
| 91 |
+
const [colunaY, setColunaY] = useState('')
|
| 92 |
+
const [colunasX, setColunasX] = useState([])
|
| 93 |
+
const [dicotomicas, setDicotomicas] = useState([])
|
| 94 |
+
const [codigoAlocado, setCodigoAlocado] = useState([])
|
| 95 |
+
const [percentuais, setPercentuais] = useState([])
|
| 96 |
+
|
| 97 |
+
const [outliersAnteriores, setOutliersAnteriores] = useState([])
|
| 98 |
+
const [iteracao, setIteracao] = useState(1)
|
| 99 |
+
|
| 100 |
+
const [selection, setSelection] = useState(null)
|
| 101 |
+
const [grauCoef, setGrauCoef] = useState(1)
|
| 102 |
+
const [grauF, setGrauF] = useState(0)
|
| 103 |
+
|
| 104 |
+
const [transformacaoY, setTransformacaoY] = useState('(x)')
|
| 105 |
+
const [transformacoesX, setTransformacoesX] = useState({})
|
| 106 |
+
|
| 107 |
+
const [fit, setFit] = useState(null)
|
| 108 |
+
const [tipoDispersao, setTipoDispersao] = useState('Variáveis Independentes Transformadas X Variável Dependente Transformada')
|
| 109 |
+
|
| 110 |
+
const [filtros, setFiltros] = useState(defaultFiltros())
|
| 111 |
+
const [outliersTexto, setOutliersTexto] = useState('')
|
| 112 |
+
const [reincluirTexto, setReincluirTexto] = useState('')
|
| 113 |
+
const [resumoOutliers, setResumoOutliers] = useState('Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0')
|
| 114 |
+
const [outliersHtml, setOutliersHtml] = useState('')
|
| 115 |
+
|
| 116 |
+
const [camposAvaliacao, setCamposAvaliacao] = useState([])
|
| 117 |
+
const [valoresAvaliacao, setValoresAvaliacao] = useState({})
|
| 118 |
+
const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
|
| 119 |
+
const [baseChoices, setBaseChoices] = useState([])
|
| 120 |
+
const [baseValue, setBaseValue] = useState('')
|
| 121 |
+
const [deleteAvalIndex, setDeleteAvalIndex] = useState('')
|
| 122 |
+
|
| 123 |
+
const [nomeArquivoExport, setNomeArquivoExport] = useState('modelo_mesa')
|
| 124 |
+
const [avaliadores, setAvaliadores] = useState([])
|
| 125 |
+
const [avaliadorSelecionado, setAvaliadorSelecionado] = useState('')
|
| 126 |
+
const [tipoFonteDados, setTipoFonteDados] = useState('')
|
| 127 |
+
const marcarTodasXRef = useRef(null)
|
| 128 |
+
const classificarXReqRef = useRef(0)
|
| 129 |
+
|
| 130 |
+
const mapaChoices = useMemo(() => ['Visualização Padrão', ...colunasNumericas], [colunasNumericas])
|
| 131 |
+
const colunasXDisponiveis = useMemo(
|
| 132 |
+
() => colunasNumericas.filter((coluna) => coluna !== colunaY),
|
| 133 |
+
[colunasNumericas, colunaY],
|
| 134 |
+
)
|
| 135 |
+
const todasXMarcadas = useMemo(
|
| 136 |
+
() => colunasXDisponiveis.length > 0 && colunasXDisponiveis.every((coluna) => colunasX.includes(coluna)),
|
| 137 |
+
[colunasXDisponiveis, colunasX],
|
| 138 |
+
)
|
| 139 |
+
const algumaXMarcada = useMemo(
|
| 140 |
+
() => colunasX.some((coluna) => colunasXDisponiveis.includes(coluna)),
|
| 141 |
+
[colunasX, colunasXDisponiveis],
|
| 142 |
+
)
|
| 143 |
+
const conselhoRegistro = useMemo(() => formatConselhoRegistro(elaborador), [elaborador])
|
| 144 |
+
const elaboradorMeta = useMemo(() => {
|
| 145 |
+
if (!elaborador) return []
|
| 146 |
+
return [elaborador.cargo, conselhoRegistro, elaborador.matricula_sem_digito ? `Matricula ${elaborador.matricula_sem_digito}` : '', elaborador.lotacao].filter(Boolean)
|
| 147 |
+
}, [elaborador, conselhoRegistro])
|
| 148 |
+
const transformacaoYBadge = useMemo(() => formatTransformacaoBadge(transformacaoY), [transformacaoY])
|
| 149 |
+
const variaveisIndependentesBadge = useMemo(() => {
|
| 150 |
+
return colunasX.map((coluna) => ({
|
| 151 |
+
coluna,
|
| 152 |
+
transformacao: formatTransformacaoBadge(transformacoesX[coluna]),
|
| 153 |
+
}))
|
| 154 |
+
}, [colunasX, transformacoesX])
|
| 155 |
+
const showCoordsPanel = Boolean(
|
| 156 |
+
coordsInfo && (
|
| 157 |
+
!coordsInfo.tem_coords ||
|
| 158 |
+
coordsMode === 'geocodificar' ||
|
| 159 |
+
geoStatusHtml ||
|
| 160 |
+
geoFalhasHtml ||
|
| 161 |
+
geoCorrecoes.length > 0 ||
|
| 162 |
+
manualMapError ||
|
| 163 |
+
geoProcessError
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
useEffect(() => {
|
| 168 |
+
if (coordsInfo && !coordsInfo.tem_coords) {
|
| 169 |
+
setCoordsMode('menu')
|
| 170 |
+
}
|
| 171 |
+
}, [coordsInfo])
|
| 172 |
+
|
| 173 |
+
useEffect(() => {
|
| 174 |
+
if (marcarTodasXRef.current) {
|
| 175 |
+
marcarTodasXRef.current.indeterminate = algumaXMarcada && !todasXMarcadas
|
| 176 |
+
}
|
| 177 |
+
}, [algumaXMarcada, todasXMarcadas])
|
| 178 |
+
|
| 179 |
+
useEffect(() => {
|
| 180 |
+
if (!sessionId || tipoFonteDados === 'dai') return undefined
|
| 181 |
+
|
| 182 |
+
const selecionadas = colunasXDisponiveis.filter((coluna) => colunasX.includes(coluna))
|
| 183 |
+
if (selecionadas.length === 0) {
|
| 184 |
+
setDicotomicas([])
|
| 185 |
+
setCodigoAlocado([])
|
| 186 |
+
setPercentuais([])
|
| 187 |
+
return undefined
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
classificarXReqRef.current += 1
|
| 191 |
+
const requestId = classificarXReqRef.current
|
| 192 |
+
let ativo = true
|
| 193 |
+
|
| 194 |
+
api.classifyElaboracaoX(sessionId, selecionadas)
|
| 195 |
+
.then((resp) => {
|
| 196 |
+
if (!ativo || requestId !== classificarXReqRef.current) return
|
| 197 |
+
setDicotomicas(resp.dicotomicas || [])
|
| 198 |
+
setCodigoAlocado(resp.codigo_alocado || [])
|
| 199 |
+
setPercentuais(resp.percentuais || [])
|
| 200 |
+
})
|
| 201 |
+
.catch(() => {
|
| 202 |
+
if (!ativo || requestId !== classificarXReqRef.current) return
|
| 203 |
+
setDicotomicas((prev) => prev.filter((item) => selecionadas.includes(item)))
|
| 204 |
+
setCodigoAlocado((prev) => prev.filter((item) => selecionadas.includes(item)))
|
| 205 |
+
setPercentuais((prev) => prev.filter((item) => selecionadas.includes(item)))
|
| 206 |
+
})
|
| 207 |
+
|
| 208 |
+
return () => {
|
| 209 |
+
ativo = false
|
| 210 |
+
}
|
| 211 |
+
}, [sessionId, tipoFonteDados, colunasX, colunasXDisponiveis])
|
| 212 |
+
|
| 213 |
+
useEffect(() => {
|
| 214 |
+
let ativo = true
|
| 215 |
+
if (!sessionId) return () => {
|
| 216 |
+
ativo = false
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
async function carregarAvaliadores() {
|
| 220 |
+
try {
|
| 221 |
+
const resp = await api.getAvaliadores()
|
| 222 |
+
if (!ativo) return
|
| 223 |
+
setAvaliadores(resp.avaliadores || [])
|
| 224 |
+
} catch {
|
| 225 |
+
if (!ativo) return
|
| 226 |
+
setAvaliadores([])
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
carregarAvaliadores()
|
| 231 |
+
return () => {
|
| 232 |
+
ativo = false
|
| 233 |
+
}
|
| 234 |
+
}, [sessionId])
|
| 235 |
+
|
| 236 |
+
async function withBusy(fn) {
|
| 237 |
+
setLoading(true)
|
| 238 |
+
setError('')
|
| 239 |
+
try {
|
| 240 |
+
await fn()
|
| 241 |
+
} catch (err) {
|
| 242 |
+
setError(err.message)
|
| 243 |
+
} finally {
|
| 244 |
+
setLoading(false)
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function parseCorrecoes(table) {
|
| 249 |
+
if (!table?.rows) return []
|
| 250 |
+
return table.rows.map((row) => {
|
| 251 |
+
const linha = row['Nº Linha'] ?? row['No Linha'] ?? row['linha'] ?? row['_index']
|
| 252 |
+
return {
|
| 253 |
+
linha: Number(linha),
|
| 254 |
+
numero_corrigido: row['Nº Corrigido'] ?? row['No Corrigido'] ?? '',
|
| 255 |
+
}
|
| 256 |
+
})
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
function applyBaseResponse(resp, options = {}) {
|
| 260 |
+
const resetXSelection = Boolean(options.resetXSelection)
|
| 261 |
+
if (resp.status) setStatus(resp.status)
|
| 262 |
+
if (resp.dados) setDados(resp.dados)
|
| 263 |
+
if (resp.mapa_html) setMapaHtml(resp.mapa_html)
|
| 264 |
+
if (resp.colunas_numericas) setColunasNumericas(resp.colunas_numericas)
|
| 265 |
+
if (resp.coluna_y_padrao) setColunaY(resp.coluna_y_padrao)
|
| 266 |
+
|
| 267 |
+
if (resp.contexto && !resetXSelection) {
|
| 268 |
+
setColunaY(resp.contexto.coluna_y || resp.coluna_y_padrao || '')
|
| 269 |
+
setColunasX(resp.contexto.colunas_x || [])
|
| 270 |
+
setDicotomicas(resp.contexto.dicotomicas || [])
|
| 271 |
+
setCodigoAlocado(resp.contexto.codigo_alocado || [])
|
| 272 |
+
setPercentuais(resp.contexto.percentuais || [])
|
| 273 |
+
setOutliersAnteriores(resp.contexto.outliers_anteriores || [])
|
| 274 |
+
setIteracao(resp.contexto.iteracao || 1)
|
| 275 |
+
} else if (resetXSelection) {
|
| 276 |
+
setColunasX([])
|
| 277 |
+
setDicotomicas([])
|
| 278 |
+
setCodigoAlocado([])
|
| 279 |
+
setPercentuais([])
|
| 280 |
+
setTransformacaoY('(x)')
|
| 281 |
+
setTransformacoesX({})
|
| 282 |
+
setOutliersAnteriores([])
|
| 283 |
+
setIteracao(1)
|
| 284 |
+
setSelection(null)
|
| 285 |
+
setFit(null)
|
| 286 |
+
setCamposAvaliacao([])
|
| 287 |
+
setResultadoAvaliacaoHtml('')
|
| 288 |
+
setOutliersHtml('')
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
if (resp.coords) {
|
| 292 |
+
setCoordsInfo(resp.coords)
|
| 293 |
+
setManualLat(resp.coords.colunas_disponiveis?.[0] || '')
|
| 294 |
+
setManualLon(resp.coords.colunas_disponiveis?.[1] || '')
|
| 295 |
+
setGeoCdlog(resp.coords.cdlog_auto || '')
|
| 296 |
+
setGeoNum(resp.coords.num_auto || '')
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
if (resp.selection && !resetXSelection) {
|
| 300 |
+
applySelectionResponse(resp.selection)
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
if (resp.fit && !resetXSelection) {
|
| 304 |
+
applyFitResponse(resp.fit)
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
function applySelectionResponse(resp) {
|
| 309 |
+
setSelection(resp)
|
| 310 |
+
if (resp.transformacao_y) {
|
| 311 |
+
setTransformacaoY(resp.transformacao_y)
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
const map = {}
|
| 315 |
+
;(resp.transform_fields || []).forEach((field) => {
|
| 316 |
+
map[field.coluna] = field.valor || '(x)'
|
| 317 |
+
})
|
| 318 |
+
setTransformacoesX(map)
|
| 319 |
+
|
| 320 |
+
if (resp.busca) {
|
| 321 |
+
setGrauCoef(resp.busca.grau_coef ?? 1)
|
| 322 |
+
setGrauF(resp.busca.grau_f ?? 0)
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
if (resp.resumo_outliers) {
|
| 326 |
+
setResumoOutliers(resp.resumo_outliers)
|
| 327 |
+
}
|
| 328 |
+
if (typeof resp.outliers_html !== 'undefined') {
|
| 329 |
+
setOutliersHtml(resp.outliers_html || '')
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
if (resp.mapa_html) {
|
| 333 |
+
setMapaHtml(resp.mapa_html)
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
function applyFitResponse(resp) {
|
| 338 |
+
setFit(resp)
|
| 339 |
+
setResumoOutliers(resp.resumo_outliers || resumoOutliers)
|
| 340 |
+
setCamposAvaliacao(resp.avaliacao_campos || [])
|
| 341 |
+
const init = {}
|
| 342 |
+
;(resp.avaliacao_campos || []).forEach((campo) => {
|
| 343 |
+
init[campo.coluna] = ''
|
| 344 |
+
})
|
| 345 |
+
setValoresAvaliacao(init)
|
| 346 |
+
setResultadoAvaliacaoHtml('')
|
| 347 |
+
setBaseChoices([])
|
| 348 |
+
setBaseValue('')
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
async function onUploadClick() {
|
| 352 |
+
if (!uploadedFile || !sessionId) return
|
| 353 |
+
await withBusy(async () => {
|
| 354 |
+
const nomeArquivo = String(uploadedFile?.name || '').toLowerCase()
|
| 355 |
+
const uploadEhDai = nomeArquivo.endsWith('.dai')
|
| 356 |
+
setTipoFonteDados(uploadEhDai ? 'dai' : 'tabular')
|
| 357 |
+
const resp = await api.uploadElaboracaoFile(sessionId, uploadedFile)
|
| 358 |
+
setManualMapError('')
|
| 359 |
+
setGeoProcessError('')
|
| 360 |
+
setGeoStatusHtml('')
|
| 361 |
+
setGeoFalhasHtml('')
|
| 362 |
+
setGeoCorrecoes([])
|
| 363 |
+
setElaborador(resp.elaborador || null)
|
| 364 |
+
setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
|
| 365 |
+
setRequiresSheet(Boolean(resp.requires_sheet))
|
| 366 |
+
setSheetOptions(resp.sheets || [])
|
| 367 |
+
setSelectedSheet(resp.sheet_selected || '')
|
| 368 |
+
if (!resp.requires_sheet) {
|
| 369 |
+
const origemResp = String(resp.tipo || '').toLowerCase()
|
| 370 |
+
setTipoFonteDados(origemResp === 'dai' ? 'dai' : 'tabular')
|
| 371 |
+
const resetXSelection = String(resp.tipo || '').toLowerCase() !== 'dai'
|
| 372 |
+
applyBaseResponse(resp, { resetXSelection })
|
| 373 |
+
} else if (resp.status) {
|
| 374 |
+
setTipoFonteDados('tabular')
|
| 375 |
+
setSelection(null)
|
| 376 |
+
setFit(null)
|
| 377 |
+
setColunasX([])
|
| 378 |
+
setDicotomicas([])
|
| 379 |
+
setCodigoAlocado([])
|
| 380 |
+
setPercentuais([])
|
| 381 |
+
setTransformacaoY('(x)')
|
| 382 |
+
setTransformacoesX({})
|
| 383 |
+
setCamposAvaliacao([])
|
| 384 |
+
setResultadoAvaliacaoHtml('')
|
| 385 |
+
setStatus(resp.status)
|
| 386 |
+
}
|
| 387 |
+
})
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
async function onConfirmSheet() {
|
| 391 |
+
if (!selectedSheet || !sessionId) return
|
| 392 |
+
await withBusy(async () => {
|
| 393 |
+
const resp = await api.confirmSheet(sessionId, selectedSheet)
|
| 394 |
+
setTipoFonteDados('tabular')
|
| 395 |
+
setManualMapError('')
|
| 396 |
+
setGeoProcessError('')
|
| 397 |
+
setGeoStatusHtml('')
|
| 398 |
+
setGeoFalhasHtml('')
|
| 399 |
+
setGeoCorrecoes([])
|
| 400 |
+
setElaborador(resp.elaborador || null)
|
| 401 |
+
setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
|
| 402 |
+
setRequiresSheet(false)
|
| 403 |
+
applyBaseResponse(resp, { resetXSelection: true })
|
| 404 |
+
})
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
async function onMapCoords() {
|
| 408 |
+
if (!manualLat || !manualLon || !sessionId) return
|
| 409 |
+
setLoading(true)
|
| 410 |
+
setError('')
|
| 411 |
+
setManualMapError('')
|
| 412 |
+
setGeoProcessError('')
|
| 413 |
+
try {
|
| 414 |
+
const resp = await api.mapCoords(sessionId, manualLat, manualLon)
|
| 415 |
+
setStatus(resp.status)
|
| 416 |
+
setMapaHtml(resp.mapa_html)
|
| 417 |
+
setDados(resp.dados)
|
| 418 |
+
setCoordsInfo(resp.coords)
|
| 419 |
+
setGeoStatusHtml('')
|
| 420 |
+
setGeoFalhasHtml('')
|
| 421 |
+
setGeoCorrecoes([])
|
| 422 |
+
} catch (err) {
|
| 423 |
+
setManualMapError(err.message || 'Falha ao mapear coordenadas.')
|
| 424 |
+
} finally {
|
| 425 |
+
setLoading(false)
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
async function onGeocodificar() {
|
| 430 |
+
if (!geoCdlog || !geoNum || !sessionId) return
|
| 431 |
+
setLoading(true)
|
| 432 |
+
setError('')
|
| 433 |
+
setManualMapError('')
|
| 434 |
+
setGeoProcessError('')
|
| 435 |
+
try {
|
| 436 |
+
const resp = await api.geocodificar(sessionId, geoCdlog, geoNum, geoAuto200)
|
| 437 |
+
setGeoStatusHtml(resp.status_html || '')
|
| 438 |
+
setGeoFalhasHtml(resp.falhas_html || '')
|
| 439 |
+
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 440 |
+
setMapaHtml(resp.mapa_html || '')
|
| 441 |
+
setDados(resp.dados || null)
|
| 442 |
+
setCoordsInfo(resp.coords || null)
|
| 443 |
+
setCoordsMode('geocodificar')
|
| 444 |
+
} catch (err) {
|
| 445 |
+
setGeoProcessError(err.message || 'Falha ao geocodificar.')
|
| 446 |
+
} finally {
|
| 447 |
+
setLoading(false)
|
| 448 |
+
}
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
async function onAplicarCorrecoesGeo() {
|
| 452 |
+
if (!sessionId) return
|
| 453 |
+
setLoading(true)
|
| 454 |
+
setError('')
|
| 455 |
+
setManualMapError('')
|
| 456 |
+
setGeoProcessError('')
|
| 457 |
+
try {
|
| 458 |
+
const resp = await api.geocodificarCorrecoes(sessionId, geoCorrecoes, geoAuto200)
|
| 459 |
+
setGeoStatusHtml(resp.status_html || '')
|
| 460 |
+
setGeoFalhasHtml(resp.falhas_html || '')
|
| 461 |
+
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 462 |
+
setMapaHtml(resp.mapa_html || '')
|
| 463 |
+
setDados(resp.dados || null)
|
| 464 |
+
setCoordsInfo(resp.coords || null)
|
| 465 |
+
setCoordsMode('geocodificar')
|
| 466 |
+
} catch (err) {
|
| 467 |
+
setGeoProcessError(err.message || 'Falha ao aplicar correções.')
|
| 468 |
+
} finally {
|
| 469 |
+
setLoading(false)
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
async function onApplySelection() {
|
| 474 |
+
if (!sessionId || !colunaY || colunasX.length === 0) return
|
| 475 |
+
await withBusy(async () => {
|
| 476 |
+
const resp = await api.applySelection({
|
| 477 |
+
session_id: sessionId,
|
| 478 |
+
coluna_y: colunaY,
|
| 479 |
+
colunas_x: colunasX,
|
| 480 |
+
dicotomicas: dicotomicas,
|
| 481 |
+
codigo_alocado: codigoAlocado,
|
| 482 |
+
percentuais: percentuais,
|
| 483 |
+
outliers_anteriores: outliersAnteriores,
|
| 484 |
+
grau_min_coef: grauCoef,
|
| 485 |
+
grau_min_f: grauF,
|
| 486 |
+
})
|
| 487 |
+
applySelectionResponse(resp)
|
| 488 |
+
setFit(null)
|
| 489 |
+
setCamposAvaliacao([])
|
| 490 |
+
setResultadoAvaliacaoHtml('')
|
| 491 |
+
})
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
async function onSearchTransform() {
|
| 495 |
+
if (!sessionId) return
|
| 496 |
+
await withBusy(async () => {
|
| 497 |
+
const busca = await api.searchTransformations(sessionId, grauCoef, grauF)
|
| 498 |
+
setSelection((prev) => ({ ...prev, busca }))
|
| 499 |
+
setGrauCoef(busca.grau_coef ?? grauCoef)
|
| 500 |
+
setGrauF(busca.grau_f ?? grauF)
|
| 501 |
+
})
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
async function onAdoptSuggestion(idx) {
|
| 505 |
+
if (!sessionId) return
|
| 506 |
+
await withBusy(async () => {
|
| 507 |
+
const resp = await api.adoptSuggestion(sessionId, idx)
|
| 508 |
+
setTransformacaoY(resp.transformacao_y || '(x)')
|
| 509 |
+
setTransformacoesX(resp.transformacoes_x || {})
|
| 510 |
+
})
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
async function onFitModel() {
|
| 514 |
+
if (!sessionId) return
|
| 515 |
+
await withBusy(async () => {
|
| 516 |
+
const resp = await api.fitModel({
|
| 517 |
+
session_id: sessionId,
|
| 518 |
+
transformacao_y: transformacaoY,
|
| 519 |
+
transformacoes_x: transformacoesX,
|
| 520 |
+
dicotomicas,
|
| 521 |
+
codigo_alocado: codigoAlocado,
|
| 522 |
+
percentuais,
|
| 523 |
+
})
|
| 524 |
+
applyFitResponse(resp)
|
| 525 |
+
})
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
async function onTipoDispersaoChange(nextTipo) {
|
| 529 |
+
setTipoDispersao(nextTipo)
|
| 530 |
+
if (!sessionId) return
|
| 531 |
+
await withBusy(async () => {
|
| 532 |
+
const resp = await api.updateModelDispersao(sessionId, nextTipo)
|
| 533 |
+
setFit((prev) => ({ ...prev, grafico_dispersao_modelo: resp.grafico }))
|
| 534 |
+
})
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
async function onApplyOutlierFilters() {
|
| 538 |
+
if (!sessionId) return
|
| 539 |
+
await withBusy(async () => {
|
| 540 |
+
const filtrosValidos = filtros.filter((f) => f.variavel && f.operador)
|
| 541 |
+
const resp = await api.applyOutlierFilters(sessionId, filtrosValidos)
|
| 542 |
+
setOutliersTexto(resp.texto || '')
|
| 543 |
+
})
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
async function onSummaryOutliers() {
|
| 547 |
+
if (!sessionId) return
|
| 548 |
+
await withBusy(async () => {
|
| 549 |
+
const resp = await api.outlierSummary(sessionId, outliersTexto, reincluirTexto)
|
| 550 |
+
setResumoOutliers(resp.resumo)
|
| 551 |
+
})
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
async function onRestartIteration() {
|
| 555 |
+
if (!sessionId) return
|
| 556 |
+
await withBusy(async () => {
|
| 557 |
+
const resp = await api.restartOutlierIteration(sessionId, outliersTexto, reincluirTexto, grauCoef, grauF)
|
| 558 |
+
applySelectionResponse(resp)
|
| 559 |
+
setOutliersAnteriores(resp.outliers_anteriores || [])
|
| 560 |
+
setIteracao(resp.iteracao || iteracao)
|
| 561 |
+
setOutliersTexto('')
|
| 562 |
+
setReincluirTexto('')
|
| 563 |
+
setFit(null)
|
| 564 |
+
setResultadoAvaliacaoHtml('')
|
| 565 |
+
setResumoOutliers(resp.resumo_outliers || resumoOutliers)
|
| 566 |
+
if (typeof window !== 'undefined') {
|
| 567 |
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
| 568 |
+
}
|
| 569 |
+
})
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
async function onClearHistory() {
|
| 573 |
+
if (!sessionId) return
|
| 574 |
+
await withBusy(async () => {
|
| 575 |
+
const resp = await api.clearOutlierHistory(sessionId)
|
| 576 |
+
setDados(resp.dados)
|
| 577 |
+
setMapaHtml(resp.mapa_html)
|
| 578 |
+
setResumoOutliers(resp.resumo_outliers || 'Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0')
|
| 579 |
+
setOutliersAnteriores([])
|
| 580 |
+
setIteracao(1)
|
| 581 |
+
setSelection(null)
|
| 582 |
+
setFit(null)
|
| 583 |
+
setCamposAvaliacao([])
|
| 584 |
+
setResultadoAvaliacaoHtml('')
|
| 585 |
+
setOutliersHtml(resp.outliers_html || '')
|
| 586 |
+
})
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
async function onCalculateAvaliacao() {
|
| 590 |
+
if (!sessionId) return
|
| 591 |
+
await withBusy(async () => {
|
| 592 |
+
const resp = await api.evaluationCalculateElab(sessionId, valoresAvaliacao, baseValue || null)
|
| 593 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 594 |
+
setBaseChoices(resp.base_choices || [])
|
| 595 |
+
setBaseValue(resp.base_value || '')
|
| 596 |
+
})
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
async function onClearAvaliacao() {
|
| 600 |
+
if (!sessionId) return
|
| 601 |
+
await withBusy(async () => {
|
| 602 |
+
const resp = await api.evaluationClearElab(sessionId)
|
| 603 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 604 |
+
setBaseChoices(resp.base_choices || [])
|
| 605 |
+
setBaseValue(resp.base_value || '')
|
| 606 |
+
})
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
async function onDeleteAvaliacao() {
|
| 610 |
+
if (!sessionId) return
|
| 611 |
+
await withBusy(async () => {
|
| 612 |
+
const resp = await api.evaluationDeleteElab(sessionId, deleteAvalIndex, baseValue || null)
|
| 613 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 614 |
+
setBaseChoices(resp.base_choices || [])
|
| 615 |
+
setBaseValue(resp.base_value || '')
|
| 616 |
+
setDeleteAvalIndex('')
|
| 617 |
+
})
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
async function onBaseChange(value) {
|
| 621 |
+
setBaseValue(value)
|
| 622 |
+
if (!sessionId) return
|
| 623 |
+
await withBusy(async () => {
|
| 624 |
+
const resp = await api.evaluationBaseElab(sessionId, value)
|
| 625 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 626 |
+
})
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
async function onExportAvaliacoes() {
|
| 630 |
+
if (!sessionId) return
|
| 631 |
+
await withBusy(async () => {
|
| 632 |
+
const blob = await api.exportEvaluationElab(sessionId)
|
| 633 |
+
downloadBlob(blob, 'avaliacoes_mesa.xlsx')
|
| 634 |
+
})
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
async function onExportModel() {
|
| 638 |
+
if (!sessionId || !nomeArquivoExport) return
|
| 639 |
+
await withBusy(async () => {
|
| 640 |
+
const avaliadorEscolhido = avaliadores.find((item) => item.nome_completo === avaliadorSelecionado) || null
|
| 641 |
+
const blob = await api.exportModel(sessionId, nomeArquivoExport, avaliadorEscolhido || elaborador || null)
|
| 642 |
+
downloadBlob(blob, `${nomeArquivoExport.replace(/\.dai$/i, '')}.dai`)
|
| 643 |
+
})
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
async function onExportBase() {
|
| 647 |
+
if (!sessionId) return
|
| 648 |
+
await withBusy(async () => {
|
| 649 |
+
const blob = await api.exportBase(sessionId, true)
|
| 650 |
+
downloadBlob(blob, 'base_tratada.csv')
|
| 651 |
+
})
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
async function onMapVarChange(value) {
|
| 655 |
+
setMapaVariavel(value)
|
| 656 |
+
if (!sessionId) return
|
| 657 |
+
await withBusy(async () => {
|
| 658 |
+
const resp = await api.updateElaboracaoMap(sessionId, value)
|
| 659 |
+
setMapaHtml(resp.mapa_html || '')
|
| 660 |
+
})
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
function toggleSelection(setter, value) {
|
| 664 |
+
setter((prev) => {
|
| 665 |
+
if (prev.includes(value)) return prev.filter((item) => item !== value)
|
| 666 |
+
return [...prev, value]
|
| 667 |
+
})
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
function onToggleColunaX(coluna) {
|
| 671 |
+
const novaLista = colunasX.includes(coluna)
|
| 672 |
+
? colunasX.filter((item) => item !== coluna)
|
| 673 |
+
: [...colunasX, coluna]
|
| 674 |
+
setColunasX(novaLista)
|
| 675 |
+
setDicotomicas((prev) => prev.filter((item) => novaLista.includes(item)))
|
| 676 |
+
setCodigoAlocado((prev) => prev.filter((item) => novaLista.includes(item)))
|
| 677 |
+
setPercentuais((prev) => prev.filter((item) => novaLista.includes(item)))
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
function onToggleTodasX(event) {
|
| 681 |
+
const checked = event.target.checked
|
| 682 |
+
if (checked) {
|
| 683 |
+
setColunasX(colunasXDisponiveis)
|
| 684 |
+
setDicotomicas((prev) => prev.filter((item) => colunasXDisponiveis.includes(item)))
|
| 685 |
+
setCodigoAlocado((prev) => prev.filter((item) => colunasXDisponiveis.includes(item)))
|
| 686 |
+
setPercentuais((prev) => prev.filter((item) => colunasXDisponiveis.includes(item)))
|
| 687 |
+
return
|
| 688 |
+
}
|
| 689 |
+
setColunasX([])
|
| 690 |
+
setDicotomicas([])
|
| 691 |
+
setCodigoAlocado([])
|
| 692 |
+
setPercentuais([])
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
return (
|
| 696 |
+
<div className="tab-content">
|
| 697 |
+
{status ? <div className="status-line">{status}</div> : null}
|
| 698 |
+
|
| 699 |
+
<SectionBlock step="1" title="Importar Dados" subtitle="Upload de CSV, Excel ou .dai com recuperação do fluxo.">
|
| 700 |
+
<div className="section1-groups">
|
| 701 |
+
<div className="subpanel section1-group">
|
| 702 |
+
<h4>Carregar modelo</h4>
|
| 703 |
+
<div className="row">
|
| 704 |
+
<input type="file" onChange={(e) => setUploadedFile(e.target.files?.[0] ?? null)} />
|
| 705 |
+
<button onClick={onUploadClick} disabled={!uploadedFile || loading || !sessionId}>Carregar arquivo</button>
|
| 706 |
+
</div>
|
| 707 |
+
|
| 708 |
+
{requiresSheet ? (
|
| 709 |
+
<div className="row">
|
| 710 |
+
<select value={selectedSheet} onChange={(e) => setSelectedSheet(e.target.value)}>
|
| 711 |
+
{sheetOptions.map((sheet) => (
|
| 712 |
+
<option key={sheet} value={sheet}>{sheet}</option>
|
| 713 |
+
))}
|
| 714 |
+
</select>
|
| 715 |
+
<button onClick={onConfirmSheet} disabled={loading || !selectedSheet}>Confirmar aba</button>
|
| 716 |
+
</div>
|
| 717 |
+
) : null}
|
| 718 |
+
</div>
|
| 719 |
+
|
| 720 |
+
<div className="subpanel section1-group">
|
| 721 |
+
<h4>Informações do modelo</h4>
|
| 722 |
+
{elaborador?.nome_completo ? (
|
| 723 |
+
<div className="modelo-cabecalho-grid">
|
| 724 |
+
<div className="elaborador-badge">
|
| 725 |
+
<div className="elaborador-badge-title">Modelo elaborado por:</div>
|
| 726 |
+
<div className="elaborador-badge-name">{elaborador.nome_completo}</div>
|
| 727 |
+
{elaboradorMeta.length > 0 ? (
|
| 728 |
+
<div className="elaborador-badge-meta">{elaboradorMeta.join(' | ')}</div>
|
| 729 |
+
) : null}
|
| 730 |
+
</div>
|
| 731 |
+
|
| 732 |
+
{colunaY || variaveisIndependentesBadge.length > 0 ? (
|
| 733 |
+
<div className="modelo-variaveis-box">
|
| 734 |
+
<div className="elaborador-badge-title">Variáveis do modelo carregado</div>
|
| 735 |
+
{colunaY ? (
|
| 736 |
+
<div className="variavel-badge-line">
|
| 737 |
+
<span className="variavel-badge-label">Dependente:</span>
|
| 738 |
+
<span className="variavel-chip variavel-chip-y">
|
| 739 |
+
{colunaY}
|
| 740 |
+
{transformacaoYBadge ? <span className="variavel-chip-transform">{` ${transformacaoYBadge}`}</span> : null}
|
| 741 |
+
</span>
|
| 742 |
+
</div>
|
| 743 |
+
) : null}
|
| 744 |
+
{variaveisIndependentesBadge.length > 0 ? (
|
| 745 |
+
<div className="variavel-badge-line">
|
| 746 |
+
<span className="variavel-badge-label">Independentes:</span>
|
| 747 |
+
<div className="variavel-chip-wrap">
|
| 748 |
+
{variaveisIndependentesBadge.map((item) => (
|
| 749 |
+
<span key={`ind-${item.coluna}`} className="variavel-chip">
|
| 750 |
+
{item.coluna}
|
| 751 |
+
{item.transformacao ? <span className="variavel-chip-transform">{` ${item.transformacao}`}</span> : null}
|
| 752 |
+
</span>
|
| 753 |
+
))}
|
| 754 |
+
</div>
|
| 755 |
+
</div>
|
| 756 |
+
) : null}
|
| 757 |
+
</div>
|
| 758 |
+
) : null}
|
| 759 |
+
</div>
|
| 760 |
+
) : (
|
| 761 |
+
<div className="section1-empty-hint">Carregue um modelo .dai para visualizar os badges do elaborador.</div>
|
| 762 |
+
)}
|
| 763 |
+
</div>
|
| 764 |
+
|
| 765 |
+
<div className="subpanel section1-group">
|
| 766 |
+
<h4>Resolver coordenadas</h4>
|
| 767 |
+
{showCoordsPanel && coordsMode !== 'skipped' ? (
|
| 768 |
+
<>
|
| 769 |
+
{coordsInfo?.aviso_html ? <div dangerouslySetInnerHTML={{ __html: coordsInfo.aviso_html }} /> : null}
|
| 770 |
+
|
| 771 |
+
{coordsMode === 'menu' ? (
|
| 772 |
+
<div className="coords-choice-row">
|
| 773 |
+
<button
|
| 774 |
+
onClick={() => {
|
| 775 |
+
setManualMapError('')
|
| 776 |
+
setGeoProcessError('')
|
| 777 |
+
setCoordsMode('mapear')
|
| 778 |
+
}}
|
| 779 |
+
disabled={loading}
|
| 780 |
+
>
|
| 781 |
+
Mapear colunas existentes para lat/lon
|
| 782 |
+
</button>
|
| 783 |
+
<span className="coords-choice-separator">ou</span>
|
| 784 |
+
<button
|
| 785 |
+
onClick={() => {
|
| 786 |
+
setManualMapError('')
|
| 787 |
+
setGeoProcessError('')
|
| 788 |
+
setCoordsMode('geocodificar')
|
| 789 |
+
}}
|
| 790 |
+
disabled={loading}
|
| 791 |
+
>
|
| 792 |
+
Geocodificar automaticamente
|
| 793 |
+
</button>
|
| 794 |
+
<span className="coords-choice-separator">ou</span>
|
| 795 |
+
<button
|
| 796 |
+
onClick={() => {
|
| 797 |
+
setManualMapError('')
|
| 798 |
+
setGeoProcessError('')
|
| 799 |
+
setCoordsMode('confirmar-sem-coords')
|
| 800 |
+
}}
|
| 801 |
+
disabled={loading}
|
| 802 |
+
>
|
| 803 |
+
Prosseguir sem mapear coordenadas
|
| 804 |
+
</button>
|
| 805 |
+
</div>
|
| 806 |
+
) : null}
|
| 807 |
+
|
| 808 |
+
{coordsMode === 'confirmar-sem-coords' ? (
|
| 809 |
+
<div className="subpanel warning">
|
| 810 |
+
<p>
|
| 811 |
+
Funcionalidades dependentes de geolocalização podem apresentar resultados incompletos sem coordenadas.
|
| 812 |
+
Deseja prosseguir mesmo assim?
|
| 813 |
+
</p>
|
| 814 |
+
<div className="row">
|
| 815 |
+
<button
|
| 816 |
+
onClick={() => {
|
| 817 |
+
setCoordsMode('skipped')
|
| 818 |
+
setStatus('Seção 1 liberada sem coordenadas completas.')
|
| 819 |
+
}}
|
| 820 |
+
disabled={loading}
|
| 821 |
+
>
|
| 822 |
+
Confirmar e prosseguir
|
| 823 |
+
</button>
|
| 824 |
+
<button
|
| 825 |
+
onClick={() => {
|
| 826 |
+
setManualMapError('')
|
| 827 |
+
setGeoProcessError('')
|
| 828 |
+
setCoordsMode('menu')
|
| 829 |
+
}}
|
| 830 |
+
disabled={loading}
|
| 831 |
+
>
|
| 832 |
+
Voltar
|
| 833 |
+
</button>
|
| 834 |
+
</div>
|
| 835 |
+
</div>
|
| 836 |
+
) : null}
|
| 837 |
+
|
| 838 |
+
{coordsMode === 'mapear' ? (
|
| 839 |
+
<div className="subpanel">
|
| 840 |
+
<div className="row">
|
| 841 |
+
<button
|
| 842 |
+
onClick={() => {
|
| 843 |
+
setManualMapError('')
|
| 844 |
+
setCoordsMode('menu')
|
| 845 |
+
}}
|
| 846 |
+
disabled={loading}
|
| 847 |
+
>
|
| 848 |
+
Voltar
|
| 849 |
+
</button>
|
| 850 |
+
</div>
|
| 851 |
+
<h4>Mapeamento manual</h4>
|
| 852 |
+
<div className="row">
|
| 853 |
+
<select value={manualLat} onChange={(e) => setManualLat(e.target.value)}>
|
| 854 |
+
<option value="">Coluna Latitude</option>
|
| 855 |
+
{(coordsInfo?.colunas_disponiveis || []).map((col) => (
|
| 856 |
+
<option key={`lat-${col}`} value={col}>{col}</option>
|
| 857 |
+
))}
|
| 858 |
+
</select>
|
| 859 |
+
<select value={manualLon} onChange={(e) => setManualLon(e.target.value)}>
|
| 860 |
+
<option value="">Coluna Longitude</option>
|
| 861 |
+
{(coordsInfo?.colunas_disponiveis || []).map((col) => (
|
| 862 |
+
<option key={`lon-${col}`} value={col}>{col}</option>
|
| 863 |
+
))}
|
| 864 |
+
</select>
|
| 865 |
+
<button onClick={onMapCoords} disabled={loading || !manualLat || !manualLon}>Confirmar mapeamento</button>
|
| 866 |
+
</div>
|
| 867 |
+
{manualMapError ? <div className="error-line inline-error">{manualMapError}</div> : null}
|
| 868 |
+
</div>
|
| 869 |
+
) : null}
|
| 870 |
+
|
| 871 |
+
{coordsMode === 'geocodificar' ? (
|
| 872 |
+
<div className="subpanel">
|
| 873 |
+
<div className="row">
|
| 874 |
+
<button
|
| 875 |
+
onClick={() => {
|
| 876 |
+
setGeoStatusHtml('')
|
| 877 |
+
setGeoFalhasHtml('')
|
| 878 |
+
setGeoCorrecoes([])
|
| 879 |
+
setGeoProcessError('')
|
| 880 |
+
setCoordsMode('menu')
|
| 881 |
+
}}
|
| 882 |
+
disabled={loading}
|
| 883 |
+
>
|
| 884 |
+
Voltar
|
| 885 |
+
</button>
|
| 886 |
+
</div>
|
| 887 |
+
<h4>Geocodificação por eixo</h4>
|
| 888 |
+
<div className="row">
|
| 889 |
+
<select value={geoCdlog} onChange={(e) => setGeoCdlog(e.target.value)}>
|
| 890 |
+
<option value="">Coluna CDLOG/CTM</option>
|
| 891 |
+
{(coordsInfo?.colunas_disponiveis || []).map((col) => (
|
| 892 |
+
<option key={`cdlog-${col}`} value={col}>{col}</option>
|
| 893 |
+
))}
|
| 894 |
+
</select>
|
| 895 |
+
<select value={geoNum} onChange={(e) => setGeoNum(e.target.value)}>
|
| 896 |
+
<option value="">Coluna Número</option>
|
| 897 |
+
{(coordsInfo?.colunas_disponiveis || []).map((col) => (
|
| 898 |
+
<option key={`num-${col}`} value={col}>{col}</option>
|
| 899 |
+
))}
|
| 900 |
+
</select>
|
| 901 |
+
<label>
|
| 902 |
+
<input type="checkbox" checked={geoAuto200} onChange={(e) => setGeoAuto200(e.target.checked)} />
|
| 903 |
+
Auto ajustar {'<='} 200
|
| 904 |
+
</label>
|
| 905 |
+
<button onClick={onGeocodificar} disabled={loading || !geoCdlog || !geoNum}>Geocodificar</button>
|
| 906 |
+
</div>
|
| 907 |
+
{geoProcessError ? <div className="error-line inline-error">{geoProcessError}</div> : null}
|
| 908 |
+
{geoStatusHtml ? <div dangerouslySetInnerHTML={{ __html: geoStatusHtml }} /> : null}
|
| 909 |
+
{geoFalhasHtml ? <div dangerouslySetInnerHTML={{ __html: geoFalhasHtml }} /> : null}
|
| 910 |
+
{geoCorrecoes.length > 0 ? (
|
| 911 |
+
<div>
|
| 912 |
+
<h5>Correções manuais</h5>
|
| 913 |
+
<div className="geo-correcoes">
|
| 914 |
+
{geoCorrecoes.map((item, idx) => (
|
| 915 |
+
<div className="row" key={`cor-${item.linha}-${idx}`}>
|
| 916 |
+
<span>Linha {item.linha}</span>
|
| 917 |
+
<input
|
| 918 |
+
type="text"
|
| 919 |
+
value={item.numero_corrigido || ''}
|
| 920 |
+
onChange={(e) => {
|
| 921 |
+
const next = [...geoCorrecoes]
|
| 922 |
+
next[idx] = { ...next[idx], numero_corrigido: e.target.value }
|
| 923 |
+
setGeoCorrecoes(next)
|
| 924 |
+
}}
|
| 925 |
+
placeholder="Número corrigido"
|
| 926 |
+
/>
|
| 927 |
+
</div>
|
| 928 |
+
))}
|
| 929 |
+
</div>
|
| 930 |
+
<button onClick={onAplicarCorrecoesGeo} disabled={loading}>Aplicar correções</button>
|
| 931 |
+
</div>
|
| 932 |
+
) : null}
|
| 933 |
+
</div>
|
| 934 |
+
) : null}
|
| 935 |
+
</>
|
| 936 |
+
) : (
|
| 937 |
+
<div className="section1-empty-hint">Coordenadas já disponíveis ou etapa concluída.</div>
|
| 938 |
+
)}
|
| 939 |
+
</div>
|
| 940 |
+
</div>
|
| 941 |
+
</SectionBlock>
|
| 942 |
+
|
| 943 |
+
<SectionBlock
|
| 944 |
+
step="2"
|
| 945 |
+
title="Visualizar Dados"
|
| 946 |
+
subtitle="Mapa interativo e tabela prévia da base carregada."
|
| 947 |
+
aside={(
|
| 948 |
+
<div className="row compact">
|
| 949 |
+
<label>Variável no mapa</label>
|
| 950 |
+
<select value={mapaVariavel} onChange={(e) => onMapVarChange(e.target.value)}>
|
| 951 |
+
{mapaChoices.map((choice) => (
|
| 952 |
+
<option key={choice} value={choice}>{choice}</option>
|
| 953 |
+
))}
|
| 954 |
+
</select>
|
| 955 |
+
</div>
|
| 956 |
+
)}
|
| 957 |
+
>
|
| 958 |
+
<div className="pane">
|
| 959 |
+
<MapFrame html={mapaHtml} />
|
| 960 |
+
</div>
|
| 961 |
+
<div className="stack-block">
|
| 962 |
+
<h4>Dados de mercado</h4>
|
| 963 |
+
<DataTable table={dados} maxHeight={540} />
|
| 964 |
+
</div>
|
| 965 |
+
</SectionBlock>
|
| 966 |
+
|
| 967 |
+
<SectionBlock step="3" title="Selecionar Variável Dependente" subtitle="Defina a variável dependente (Y).">
|
| 968 |
+
<div className="row">
|
| 969 |
+
<label>Variável Dependente (Y)</label>
|
| 970 |
+
<select value={colunaY} onChange={(e) => setColunaY(e.target.value)}>
|
| 971 |
+
<option value="">Selecione</option>
|
| 972 |
+
{colunasNumericas.map((col) => (
|
| 973 |
+
<option key={col} value={col}>{col}</option>
|
| 974 |
+
))}
|
| 975 |
+
</select>
|
| 976 |
+
</div>
|
| 977 |
+
</SectionBlock>
|
| 978 |
+
|
| 979 |
+
<SectionBlock step="4" title="Selecionar Variáveis Independentes" subtitle="Escolha regressoras e grupos de tipologia.">
|
| 980 |
+
<div className="compact-option-group">
|
| 981 |
+
<h4>Variáveis Independentes (X)</h4>
|
| 982 |
+
<div className="checkbox-inline-wrap checkbox-inline-wrap-tools">
|
| 983 |
+
<label className="compact-checkbox compact-checkbox-toggle-all">
|
| 984 |
+
<input
|
| 985 |
+
ref={marcarTodasXRef}
|
| 986 |
+
type="checkbox"
|
| 987 |
+
checked={todasXMarcadas}
|
| 988 |
+
onChange={onToggleTodasX}
|
| 989 |
+
disabled={colunasXDisponiveis.length === 0}
|
| 990 |
+
/>
|
| 991 |
+
{todasXMarcadas ? 'Desmarcar todas' : 'Marcar todas'}
|
| 992 |
+
</label>
|
| 993 |
+
<span className="compact-selection-count">
|
| 994 |
+
{colunasX.length}/{colunasXDisponiveis.length} selecionadas
|
| 995 |
+
</span>
|
| 996 |
+
</div>
|
| 997 |
+
<div className="checkbox-inline-wrap">
|
| 998 |
+
{colunasXDisponiveis.map((col) => (
|
| 999 |
+
<label key={`x-${col}`} className="compact-checkbox">
|
| 1000 |
+
<input
|
| 1001 |
+
type="checkbox"
|
| 1002 |
+
checked={colunasX.includes(col)}
|
| 1003 |
+
onChange={() => onToggleColunaX(col)}
|
| 1004 |
+
/>
|
| 1005 |
+
{col}
|
| 1006 |
+
</label>
|
| 1007 |
+
))}
|
| 1008 |
+
</div>
|
| 1009 |
+
</div>
|
| 1010 |
+
|
| 1011 |
+
<div className="compact-option-group">
|
| 1012 |
+
<h4>Variáveis Dicotômicas (0/1)</h4>
|
| 1013 |
+
<div className="checkbox-inline-wrap">
|
| 1014 |
+
{colunasX.map((col) => (
|
| 1015 |
+
<label key={`d-${col}`} className="compact-checkbox">
|
| 1016 |
+
<input type="checkbox" checked={dicotomicas.includes(col)} onChange={() => toggleSelection(setDicotomicas, col)} />
|
| 1017 |
+
{col}
|
| 1018 |
+
</label>
|
| 1019 |
+
))}
|
| 1020 |
+
</div>
|
| 1021 |
+
</div>
|
| 1022 |
+
|
| 1023 |
+
<div className="compact-option-group">
|
| 1024 |
+
<h4>Variáveis de Código Alocado/Ajustado</h4>
|
| 1025 |
+
<div className="checkbox-inline-wrap">
|
| 1026 |
+
{colunasX.map((col) => (
|
| 1027 |
+
<label key={`c-${col}`} className="compact-checkbox">
|
| 1028 |
+
<input type="checkbox" checked={codigoAlocado.includes(col)} onChange={() => toggleSelection(setCodigoAlocado, col)} />
|
| 1029 |
+
{col}
|
| 1030 |
+
</label>
|
| 1031 |
+
))}
|
| 1032 |
+
</div>
|
| 1033 |
+
</div>
|
| 1034 |
+
|
| 1035 |
+
<div className="compact-option-group">
|
| 1036 |
+
<h4>Variáveis Percentuais (0 a 1)</h4>
|
| 1037 |
+
<div className="checkbox-inline-wrap">
|
| 1038 |
+
{colunasX.map((col) => (
|
| 1039 |
+
<label key={`p-${col}`} className="compact-checkbox">
|
| 1040 |
+
<input type="checkbox" checked={percentuais.includes(col)} onChange={() => toggleSelection(setPercentuais, col)} />
|
| 1041 |
+
{col}
|
| 1042 |
+
</label>
|
| 1043 |
+
))}
|
| 1044 |
+
</div>
|
| 1045 |
+
</div>
|
| 1046 |
+
|
| 1047 |
+
<div className="row">
|
| 1048 |
+
<button onClick={onApplySelection} disabled={loading || !colunaY || colunasX.length === 0}>Aplicar seleção</button>
|
| 1049 |
+
</div>
|
| 1050 |
+
</SectionBlock>
|
| 1051 |
+
|
| 1052 |
+
{selection ? (
|
| 1053 |
+
<>
|
| 1054 |
+
<SectionBlock step="5" title="Estatísticas das Variáveis Selecionadas" subtitle="Resumo estatístico para Y e regressoras.">
|
| 1055 |
+
<DataTable table={selection.estatisticas} />
|
| 1056 |
+
</SectionBlock>
|
| 1057 |
+
|
| 1058 |
+
<SectionBlock step="6" title="Teste de Micronumerosidade" subtitle="Validação de amostra mínima para variáveis selecionadas.">
|
| 1059 |
+
<div dangerouslySetInnerHTML={{ __html: selection.micronumerosidade_html || '' }} />
|
| 1060 |
+
{selection.aviso_multicolinearidade?.visible ? (
|
| 1061 |
+
<div dangerouslySetInnerHTML={{ __html: selection.aviso_multicolinearidade.html }} />
|
| 1062 |
+
) : null}
|
| 1063 |
+
</SectionBlock>
|
| 1064 |
+
|
| 1065 |
+
<SectionBlock step="7" title="Gráficos de Dispersão das Variáveis Independentes" subtitle="Leitura visual entre X e Y no conjunto filtrado.">
|
| 1066 |
+
<PlotFigure
|
| 1067 |
+
figure={selection.grafico_dispersao}
|
| 1068 |
+
title="Dispersão (dados filtrados)"
|
| 1069 |
+
forceHideLegend
|
| 1070 |
+
className="plot-stretch"
|
| 1071 |
+
/>
|
| 1072 |
+
</SectionBlock>
|
| 1073 |
+
|
| 1074 |
+
<SectionBlock step="8" title="Transformações Sugeridas" subtitle="Busca automática de combinações por R² e enquadramento.">
|
| 1075 |
+
<div className="row">
|
| 1076 |
+
<label>Grau mínimo dos coeficientes</label>
|
| 1077 |
+
<select value={grauCoef} onChange={(e) => setGrauCoef(Number(e.target.value))}>
|
| 1078 |
+
{GRAUS_COEF.map((item) => (
|
| 1079 |
+
<option key={`coef-${item.value}`} value={item.value}>{item.label}</option>
|
| 1080 |
+
))}
|
| 1081 |
+
</select>
|
| 1082 |
+
<label>Grau mínimo do teste F</label>
|
| 1083 |
+
<select value={grauF} onChange={(e) => setGrauF(Number(e.target.value))}>
|
| 1084 |
+
{GRAUS_F.map((item) => (
|
| 1085 |
+
<option key={`f-${item.value}`} value={item.value}>{item.label}</option>
|
| 1086 |
+
))}
|
| 1087 |
+
</select>
|
| 1088 |
+
<button onClick={onSearchTransform} disabled={loading}>Buscar transformações</button>
|
| 1089 |
+
</div>
|
| 1090 |
+
{(selection.busca?.resultados || []).length > 0 ? (
|
| 1091 |
+
<div className="transform-suggestions-grid">
|
| 1092 |
+
{(selection.busca?.resultados || []).map((item, idx) => (
|
| 1093 |
+
<div key={`sug-${item.rank || idx + 1}`} className="transform-suggestion-card">
|
| 1094 |
+
<div className="transform-suggestion-head">
|
| 1095 |
+
<span className="transform-suggestion-rank">#{item.rank || idx + 1}</span>
|
| 1096 |
+
<span className="transform-suggestion-r2">R² = {Number(item.r2 ?? 0).toFixed(4)}</span>
|
| 1097 |
+
</div>
|
| 1098 |
+
<div className="transform-suggestion-line"><strong>Y:</strong> {item.transformacao_y}</div>
|
| 1099 |
+
<div className="transform-suggestion-list">
|
| 1100 |
+
{Object.entries(item.transformacoes_x || {}).map(([coluna, transf]) => {
|
| 1101 |
+
const grau = Number(item.graus_coef?.[coluna] ?? 0)
|
| 1102 |
+
return (
|
| 1103 |
+
<div key={`scol-${item.rank || idx}-${coluna}`} className="transform-suggestion-item">
|
| 1104 |
+
<span className="transform-suggestion-col">{coluna}</span>
|
| 1105 |
+
<span className="transform-suggestion-fn">{transf}</span>
|
| 1106 |
+
<span className={grauBadgeClass(grau)}>{GRAU_LABEL_CURTO[grau] || 'Sem enq.'}</span>
|
| 1107 |
+
</div>
|
| 1108 |
+
)
|
| 1109 |
+
})}
|
| 1110 |
+
</div>
|
| 1111 |
+
<div className="transform-suggestion-foot">
|
| 1112 |
+
<span className={grauBadgeClass(Number(item.grau_f ?? 0))}>
|
| 1113 |
+
Teste F: {GRAU_LABEL_CURTO[Number(item.grau_f ?? 0)] || 'Sem enq.'}
|
| 1114 |
+
</span>
|
| 1115 |
+
</div>
|
| 1116 |
+
<button onClick={() => onAdoptSuggestion(idx)} disabled={loading}>
|
| 1117 |
+
Adotar sugestão #{item.rank || idx + 1}
|
| 1118 |
+
</button>
|
| 1119 |
+
</div>
|
| 1120 |
+
))}
|
| 1121 |
+
</div>
|
| 1122 |
+
) : (
|
| 1123 |
+
<div dangerouslySetInnerHTML={{ __html: selection.busca?.html || '' }} />
|
| 1124 |
+
)}
|
| 1125 |
+
</SectionBlock>
|
| 1126 |
+
|
| 1127 |
+
<SectionBlock step="9" title="Aplicação das Transformações" subtitle="Configuração manual para ajuste do modelo.">
|
| 1128 |
+
<div className="row">
|
| 1129 |
+
<label>Transformação de Y</label>
|
| 1130 |
+
<select value={transformacaoY} onChange={(e) => setTransformacaoY(e.target.value)}>
|
| 1131 |
+
{['(x)', '1/(x)', 'ln(x)', 'exp(x)', '(x)^2', 'raiz(x)', '1/raiz(x)'].map((item) => (
|
| 1132 |
+
<option key={`y-${item}`} value={item}>{item}</option>
|
| 1133 |
+
))}
|
| 1134 |
+
</select>
|
| 1135 |
+
</div>
|
| 1136 |
+
|
| 1137 |
+
<div className="transform-grid">
|
| 1138 |
+
{(selection.transform_fields || []).map((field) => (
|
| 1139 |
+
<div key={`tf-${field.coluna}`} className="transform-card">
|
| 1140 |
+
<span>{field.coluna}</span>
|
| 1141 |
+
<select
|
| 1142 |
+
value={transformacoesX[field.coluna] || '(x)'}
|
| 1143 |
+
onChange={(e) => setTransformacoesX((prev) => ({ ...prev, [field.coluna]: e.target.value }))}
|
| 1144 |
+
disabled={field.locked}
|
| 1145 |
+
>
|
| 1146 |
+
{(field.choices || []).map((choice) => (
|
| 1147 |
+
<option key={`${field.coluna}-${choice}`} value={choice}>{choice}</option>
|
| 1148 |
+
))}
|
| 1149 |
+
</select>
|
| 1150 |
+
</div>
|
| 1151 |
+
))}
|
| 1152 |
+
</div>
|
| 1153 |
+
|
| 1154 |
+
<button onClick={onFitModel} disabled={loading}>Aplicar transformações e ajustar modelo</button>
|
| 1155 |
+
</SectionBlock>
|
| 1156 |
+
</>
|
| 1157 |
+
) : null}
|
| 1158 |
+
|
| 1159 |
+
{fit ? (
|
| 1160 |
+
<>
|
| 1161 |
+
<SectionBlock step="10" title="Gráficos de Dispersão (Variáveis Transformadas)" subtitle="Dispersão com variáveis já transformadas.">
|
| 1162 |
+
<div className="row">
|
| 1163 |
+
<label>Tipo de dispersão</label>
|
| 1164 |
+
<select value={tipoDispersao} onChange={(e) => onTipoDispersaoChange(e.target.value)}>
|
| 1165 |
+
<option value="Variáveis Independentes Transformadas X Variável Dependente Transformada">X transformado x Y transformado</option>
|
| 1166 |
+
<option value="Variáveis Independentes Transformadas X Resíduo Padronizado">X transformado x Resíduo</option>
|
| 1167 |
+
</select>
|
| 1168 |
+
</div>
|
| 1169 |
+
<PlotFigure figure={fit.grafico_dispersao_modelo} title="Dispersão do modelo" forceHideLegend className="plot-stretch" />
|
| 1170 |
+
</SectionBlock>
|
| 1171 |
+
|
| 1172 |
+
<SectionBlock step="11" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
|
| 1173 |
+
<div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
|
| 1174 |
+
<div className="two-col diagnostic-tables">
|
| 1175 |
+
<div className="pane">
|
| 1176 |
+
<h4>Tabela de Coeficientes</h4>
|
| 1177 |
+
<DataTable table={fit.tabela_coef} maxHeight={460} />
|
| 1178 |
+
</div>
|
| 1179 |
+
<div className="pane">
|
| 1180 |
+
<h4>Valores Observados x Calculados</h4>
|
| 1181 |
+
<DataTable table={fit.tabela_obs_calc} maxHeight={460} />
|
| 1182 |
+
</div>
|
| 1183 |
+
</div>
|
| 1184 |
+
</SectionBlock>
|
| 1185 |
+
|
| 1186 |
+
<SectionBlock step="12" title="Gráficos de Diagnóstico do Modelo" subtitle="Obs x calc, resíduos, histograma, Cook e correlação.">
|
| 1187 |
+
<div className="plot-grid-2-fixed">
|
| 1188 |
+
<PlotFigure figure={fit.grafico_obs_calc} title="Obs x Calc" />
|
| 1189 |
+
<PlotFigure figure={fit.grafico_residuos} title="Resíduos" />
|
| 1190 |
+
<PlotFigure figure={fit.grafico_histograma} title="Histograma" />
|
| 1191 |
+
<PlotFigure figure={fit.grafico_cook} title="Cook" forceHideLegend />
|
| 1192 |
+
</div>
|
| 1193 |
+
<div className="plot-full-width">
|
| 1194 |
+
<PlotFigure figure={fit.grafico_correlacao} title="Matriz de correlação" className="plot-correlation-card" />
|
| 1195 |
+
</div>
|
| 1196 |
+
</SectionBlock>
|
| 1197 |
+
|
| 1198 |
+
<SectionBlock step="13" title="Analisar Outliers" subtitle="Métricas para identificação de observações influentes.">
|
| 1199 |
+
<DataTable table={fit.tabela_metricas} maxHeight={320} />
|
| 1200 |
+
</SectionBlock>
|
| 1201 |
+
|
| 1202 |
+
<SectionBlock step="14" title="Exclusão ou Reinclusão de Outliers" subtitle="Filtre índices, revise e atualize o modelo.">
|
| 1203 |
+
{outliersAnteriores.length > 0 && outliersHtml ? (
|
| 1204 |
+
<div className="outliers-html-box" dangerouslySetInnerHTML={{ __html: outliersHtml }} />
|
| 1205 |
+
) : null}
|
| 1206 |
+
<div className="outlier-subheader">Filtrar outliers</div>
|
| 1207 |
+
<div className="outlier-dica">Outliers = linhas que satisfazem qualquer filtro (lógica OR / união).</div>
|
| 1208 |
+
<div className="filtros-stack">
|
| 1209 |
+
{filtros.map((filtro, idx) => (
|
| 1210 |
+
<div key={`filtro-${idx}`} className="filtro-row-react">
|
| 1211 |
+
<select
|
| 1212 |
+
value={filtro.variavel}
|
| 1213 |
+
onChange={(e) => {
|
| 1214 |
+
const next = [...filtros]
|
| 1215 |
+
next[idx] = { ...next[idx], variavel: e.target.value }
|
| 1216 |
+
setFiltros(next)
|
| 1217 |
+
}}
|
| 1218 |
+
>
|
| 1219 |
+
{(fit.variaveis_filtro || []).map((varItem) => (
|
| 1220 |
+
<option key={`vf-${idx}-${varItem}`} value={varItem}>{varItem}</option>
|
| 1221 |
+
))}
|
| 1222 |
+
</select>
|
| 1223 |
+
<select
|
| 1224 |
+
value={filtro.operador}
|
| 1225 |
+
onChange={(e) => {
|
| 1226 |
+
const next = [...filtros]
|
| 1227 |
+
next[idx] = { ...next[idx], operador: e.target.value }
|
| 1228 |
+
setFiltros(next)
|
| 1229 |
+
}}
|
| 1230 |
+
>
|
| 1231 |
+
{OPERADORES.map((op) => (
|
| 1232 |
+
<option key={`op-${idx}-${op}`} value={op}>{op}</option>
|
| 1233 |
+
))}
|
| 1234 |
+
</select>
|
| 1235 |
+
<input
|
| 1236 |
+
type="number"
|
| 1237 |
+
value={filtro.valor}
|
| 1238 |
+
onChange={(e) => {
|
| 1239 |
+
const next = [...filtros]
|
| 1240 |
+
next[idx] = { ...next[idx], valor: Number(e.target.value) }
|
| 1241 |
+
setFiltros(next)
|
| 1242 |
+
}}
|
| 1243 |
+
/>
|
| 1244 |
+
</div>
|
| 1245 |
+
))}
|
| 1246 |
+
</div>
|
| 1247 |
+
<div className="outlier-actions-row">
|
| 1248 |
+
<button onClick={onApplyOutlierFilters} disabled={loading}>Aplicar filtros</button>
|
| 1249 |
+
</div>
|
| 1250 |
+
|
| 1251 |
+
<div className="outlier-divider"><span className="arrow">→</span></div>
|
| 1252 |
+
<div className="outlier-subheader">Excluir ou reincluir índices</div>
|
| 1253 |
+
<div className="outlier-inputs-grid">
|
| 1254 |
+
<div className="outlier-input-card">
|
| 1255 |
+
<label>A excluir</label>
|
| 1256 |
+
<input type="text" value={outliersTexto} onChange={(e) => setOutliersTexto(e.target.value)} placeholder="ex: 5, 12, 30" />
|
| 1257 |
+
</div>
|
| 1258 |
+
<div className="outlier-input-card">
|
| 1259 |
+
<label>A reincluir</label>
|
| 1260 |
+
<input type="text" value={reincluirTexto} onChange={(e) => setReincluirTexto(e.target.value)} placeholder="ex: 5" />
|
| 1261 |
+
</div>
|
| 1262 |
+
</div>
|
| 1263 |
+
|
| 1264 |
+
<div className="outlier-actions-row">
|
| 1265 |
+
<button onClick={onRestartIteration} disabled={loading} className="btn-reiniciar-iteracao">
|
| 1266 |
+
Atualizar Modelo (Excluir/Reincluir Outliers)
|
| 1267 |
+
</button>
|
| 1268 |
+
</div>
|
| 1269 |
+
<div className="resumo-outliers-box">Iteração: {iteracao} | {resumoOutliers}</div>
|
| 1270 |
+
<div className="resumo-outliers-box">Outliers anteriores: {joinSelection(outliersAnteriores) || '-'}</div>
|
| 1271 |
+
</SectionBlock>
|
| 1272 |
+
|
| 1273 |
+
<SectionBlock step="15" title="Avaliação de Imóvel" subtitle="Cálculo individual e comparação entre avaliações.">
|
| 1274 |
+
<div className="avaliacao-grid">
|
| 1275 |
+
{camposAvaliacao.map((campo) => (
|
| 1276 |
+
<div key={`aval-${campo.coluna}`} className="avaliacao-card">
|
| 1277 |
+
<label>{campo.coluna}</label>
|
| 1278 |
+
<input
|
| 1279 |
+
type="number"
|
| 1280 |
+
value={valoresAvaliacao[campo.coluna] ?? ''}
|
| 1281 |
+
placeholder={campo.placeholder || ''}
|
| 1282 |
+
onChange={(e) => setValoresAvaliacao((prev) => ({ ...prev, [campo.coluna]: e.target.value }))}
|
| 1283 |
+
/>
|
| 1284 |
+
</div>
|
| 1285 |
+
))}
|
| 1286 |
+
</div>
|
| 1287 |
+
<div className="row-wrap avaliacao-actions-row">
|
| 1288 |
+
<button onClick={onCalculateAvaliacao} disabled={loading}>Calcular</button>
|
| 1289 |
+
<button onClick={onClearAvaliacao} disabled={loading}>Limpar</button>
|
| 1290 |
+
<button onClick={onExportAvaliacoes} disabled={loading}>Exportar avaliações (Excel)</button>
|
| 1291 |
+
</div>
|
| 1292 |
+
<div className="row avaliacao-base-row">
|
| 1293 |
+
<label>Base comparação</label>
|
| 1294 |
+
<select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
|
| 1295 |
+
<option value="">Selecione</option>
|
| 1296 |
+
{baseChoices.map((choice) => (
|
| 1297 |
+
<option key={`base-${choice}`} value={choice}>{choice}</option>
|
| 1298 |
+
))}
|
| 1299 |
+
</select>
|
| 1300 |
+
<label>Excluir avaliação #</label>
|
| 1301 |
+
<input type="text" value={deleteAvalIndex} onChange={(e) => setDeleteAvalIndex(e.target.value)} />
|
| 1302 |
+
<button onClick={onDeleteAvaliacao} disabled={loading}>Excluir</button>
|
| 1303 |
+
</div>
|
| 1304 |
+
<div className="avaliacao-resultado-box" dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }} />
|
| 1305 |
+
</SectionBlock>
|
| 1306 |
+
|
| 1307 |
+
<SectionBlock step="16" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
|
| 1308 |
+
<div className="row">
|
| 1309 |
+
<label>Nome do arquivo (.dai)</label>
|
| 1310 |
+
<input type="text" value={nomeArquivoExport} onChange={(e) => setNomeArquivoExport(e.target.value)} />
|
| 1311 |
+
<label>Avaliador</label>
|
| 1312 |
+
<select value={avaliadorSelecionado} onChange={(e) => setAvaliadorSelecionado(e.target.value)}>
|
| 1313 |
+
<option value="">Manter elaborador do modelo (se houver)</option>
|
| 1314 |
+
{avaliadores.map((item) => (
|
| 1315 |
+
<option key={`aval-exp-${item.nome_completo}`} value={item.nome_completo}>
|
| 1316 |
+
{item.nome_completo}
|
| 1317 |
+
</option>
|
| 1318 |
+
))}
|
| 1319 |
+
</select>
|
| 1320 |
+
<button onClick={onExportModel} disabled={loading || !nomeArquivoExport}>Exportar modelo</button>
|
| 1321 |
+
<button onClick={onExportBase} disabled={loading}>Exportar base CSV</button>
|
| 1322 |
+
</div>
|
| 1323 |
+
</SectionBlock>
|
| 1324 |
+
</>
|
| 1325 |
+
) : null}
|
| 1326 |
+
|
| 1327 |
+
{loading ? <div className="status-line">Processando...</div> : null}
|
| 1328 |
+
{error ? <div className="error-line">{error}</div> : null}
|
| 1329 |
+
</div>
|
| 1330 |
+
)
|
| 1331 |
+
}
|
frontend/src/components/MapFrame.jsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
|
| 3 |
+
export default function MapFrame({ html }) {
|
| 4 |
+
if (!html) {
|
| 5 |
+
return <div className="empty-box">Mapa indisponivel.</div>
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<iframe
|
| 10 |
+
title="mapa"
|
| 11 |
+
className="map-frame"
|
| 12 |
+
srcDoc={html}
|
| 13 |
+
sandbox="allow-scripts allow-same-origin allow-popups"
|
| 14 |
+
/>
|
| 15 |
+
)
|
| 16 |
+
}
|
frontend/src/components/PlotFigure.jsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import Plot from 'react-plotly.js'
|
| 3 |
+
import Plotly from 'plotly.js-dist-min'
|
| 4 |
+
|
| 5 |
+
export default function PlotFigure({ figure, title, forceHideLegend = false, className = '' }) {
|
| 6 |
+
if (!figure) {
|
| 7 |
+
return <div className="empty-box">Grafico indisponivel.</div>
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const data = (figure.data || []).map((trace) => {
|
| 11 |
+
if (!forceHideLegend) return trace
|
| 12 |
+
return { ...trace, showlegend: false }
|
| 13 |
+
})
|
| 14 |
+
|
| 15 |
+
const baseLayout = figure.layout || {}
|
| 16 |
+
const { width: _ignoreWidth, ...layoutNoWidth } = baseLayout
|
| 17 |
+
const safeAnnotations = Array.isArray(baseLayout.annotations)
|
| 18 |
+
? baseLayout.annotations.map((annotation) => {
|
| 19 |
+
const { ax, ay, axref, ayref, arrowhead, arrowsize, arrowwidth, arrowcolor, standoff, ...clean } = annotation || {}
|
| 20 |
+
return { ...clean, showarrow: false }
|
| 21 |
+
})
|
| 22 |
+
: baseLayout.annotations
|
| 23 |
+
const layout = {
|
| 24 |
+
...layoutNoWidth,
|
| 25 |
+
autosize: true,
|
| 26 |
+
annotations: safeAnnotations,
|
| 27 |
+
margin: baseLayout.margin || { t: 40, r: 20, b: 50, l: 50 },
|
| 28 |
+
}
|
| 29 |
+
if (forceHideLegend) {
|
| 30 |
+
layout.showlegend = false
|
| 31 |
+
}
|
| 32 |
+
const plotHeight = layout.height ? `${layout.height}px` : '100%'
|
| 33 |
+
const cardClassName = `plot-card ${className}`.trim()
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<div className={cardClassName}>
|
| 37 |
+
{title ? <h4>{title}</h4> : null}
|
| 38 |
+
<Plot
|
| 39 |
+
data={data}
|
| 40 |
+
layout={layout}
|
| 41 |
+
config={{ responsive: true, displaylogo: false }}
|
| 42 |
+
style={{ width: '100%', height: plotHeight, minHeight: '320px' }}
|
| 43 |
+
useResizeHandler
|
| 44 |
+
plotly={Plotly}
|
| 45 |
+
/>
|
| 46 |
+
</div>
|
| 47 |
+
)
|
| 48 |
+
}
|
frontend/src/components/SectionBlock.jsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
|
| 3 |
+
export default function SectionBlock({
|
| 4 |
+
step,
|
| 5 |
+
title,
|
| 6 |
+
subtitle,
|
| 7 |
+
aside,
|
| 8 |
+
children,
|
| 9 |
+
}) {
|
| 10 |
+
return (
|
| 11 |
+
<section className="workflow-section" style={{ '--section-order': Number(step) || 1 }}>
|
| 12 |
+
<header className="section-head">
|
| 13 |
+
<span className="section-index">{step}</span>
|
| 14 |
+
<div className="section-title-wrap">
|
| 15 |
+
<h3>{title}</h3>
|
| 16 |
+
{subtitle ? <p>{subtitle}</p> : null}
|
| 17 |
+
</div>
|
| 18 |
+
{aside ? <div className="section-head-aside">{aside}</div> : null}
|
| 19 |
+
</header>
|
| 20 |
+
<div className="section-body">{children}</div>
|
| 21 |
+
</section>
|
| 22 |
+
)
|
| 23 |
+
}
|
frontend/src/components/VisualizacaoTab.jsx
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react'
|
| 2 |
+
import { api, downloadBlob } from '../api'
|
| 3 |
+
import DataTable from './DataTable'
|
| 4 |
+
import MapFrame from './MapFrame'
|
| 5 |
+
import PlotFigure from './PlotFigure'
|
| 6 |
+
import SectionBlock from './SectionBlock'
|
| 7 |
+
|
| 8 |
+
const INNER_TABS = [
|
| 9 |
+
{ key: 'mapa', label: 'Mapa' },
|
| 10 |
+
{ key: 'dados', label: 'Dados' },
|
| 11 |
+
{ key: 'transformacoes', label: 'Transformações' },
|
| 12 |
+
{ key: 'resumo', label: 'Resumo' },
|
| 13 |
+
{ key: 'coeficientes', label: 'Coeficientes' },
|
| 14 |
+
{ key: 'obs_calc', label: 'Obs x Calc' },
|
| 15 |
+
{ key: 'graficos', label: 'Gráficos' },
|
| 16 |
+
{ key: 'avaliacao', label: 'Avaliação' },
|
| 17 |
+
{ key: 'avaliacao_massa', label: 'Avaliação em Massa' },
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
export default function VisualizacaoTab({ sessionId }) {
|
| 21 |
+
const [loading, setLoading] = useState(false)
|
| 22 |
+
const [error, setError] = useState('')
|
| 23 |
+
const [status, setStatus] = useState('')
|
| 24 |
+
const [badgeHtml, setBadgeHtml] = useState('')
|
| 25 |
+
|
| 26 |
+
const [uploadedFile, setUploadedFile] = useState(null)
|
| 27 |
+
|
| 28 |
+
const [dados, setDados] = useState(null)
|
| 29 |
+
const [estatisticas, setEstatisticas] = useState(null)
|
| 30 |
+
const [escalasHtml, setEscalasHtml] = useState('')
|
| 31 |
+
const [dadosTransformados, setDadosTransformados] = useState(null)
|
| 32 |
+
const [resumoHtml, setResumoHtml] = useState('')
|
| 33 |
+
const [coeficientes, setCoeficientes] = useState(null)
|
| 34 |
+
const [obsCalc, setObsCalc] = useState(null)
|
| 35 |
+
|
| 36 |
+
const [plotObsCalc, setPlotObsCalc] = useState(null)
|
| 37 |
+
const [plotResiduos, setPlotResiduos] = useState(null)
|
| 38 |
+
const [plotHistograma, setPlotHistograma] = useState(null)
|
| 39 |
+
const [plotCook, setPlotCook] = useState(null)
|
| 40 |
+
const [plotCorr, setPlotCorr] = useState(null)
|
| 41 |
+
|
| 42 |
+
const [mapaHtml, setMapaHtml] = useState('')
|
| 43 |
+
const [mapaChoices, setMapaChoices] = useState(['Visualização Padrão'])
|
| 44 |
+
const [mapaVar, setMapaVar] = useState('Visualização Padrão')
|
| 45 |
+
|
| 46 |
+
const [camposAvaliacao, setCamposAvaliacao] = useState([])
|
| 47 |
+
const [valoresAvaliacao, setValoresAvaliacao] = useState({})
|
| 48 |
+
const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
|
| 49 |
+
const [baseChoices, setBaseChoices] = useState([])
|
| 50 |
+
const [baseValue, setBaseValue] = useState('')
|
| 51 |
+
const [deleteAvalIndex, setDeleteAvalIndex] = useState('')
|
| 52 |
+
|
| 53 |
+
const [activeInnerTab, setActiveInnerTab] = useState('mapa')
|
| 54 |
+
|
| 55 |
+
const statusFluxo = [
|
| 56 |
+
{ label: 'Modelo carregado', done: Boolean(status) },
|
| 57 |
+
{ label: 'Dados exibidos', done: Boolean(dados) },
|
| 58 |
+
{ label: 'Gráficos prontos', done: Boolean(plotObsCalc) },
|
| 59 |
+
{ label: 'Avaliação ativa', done: camposAvaliacao.length > 0 },
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
function resetConteudoVisualizacao() {
|
| 63 |
+
setDados(null)
|
| 64 |
+
setEstatisticas(null)
|
| 65 |
+
setEscalasHtml('')
|
| 66 |
+
setDadosTransformados(null)
|
| 67 |
+
setResumoHtml('')
|
| 68 |
+
setCoeficientes(null)
|
| 69 |
+
setObsCalc(null)
|
| 70 |
+
|
| 71 |
+
setPlotObsCalc(null)
|
| 72 |
+
setPlotResiduos(null)
|
| 73 |
+
setPlotHistograma(null)
|
| 74 |
+
setPlotCook(null)
|
| 75 |
+
setPlotCorr(null)
|
| 76 |
+
|
| 77 |
+
setMapaHtml('')
|
| 78 |
+
setMapaChoices(['Visualização Padrão'])
|
| 79 |
+
setMapaVar('Visualização Padrão')
|
| 80 |
+
|
| 81 |
+
setCamposAvaliacao([])
|
| 82 |
+
setValoresAvaliacao({})
|
| 83 |
+
setResultadoAvaliacaoHtml('')
|
| 84 |
+
setBaseChoices([])
|
| 85 |
+
setBaseValue('')
|
| 86 |
+
setDeleteAvalIndex('')
|
| 87 |
+
|
| 88 |
+
setActiveInnerTab('mapa')
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
function applyExibicaoResponse(resp) {
|
| 92 |
+
setDados(resp.dados || null)
|
| 93 |
+
setEstatisticas(resp.estatisticas || null)
|
| 94 |
+
setEscalasHtml(resp.escalas_html || '')
|
| 95 |
+
setDadosTransformados(resp.dados_transformados || null)
|
| 96 |
+
setResumoHtml(resp.resumo_html || '')
|
| 97 |
+
setCoeficientes(resp.coeficientes || null)
|
| 98 |
+
setObsCalc(resp.obs_calc || null)
|
| 99 |
+
|
| 100 |
+
setPlotObsCalc(resp.grafico_obs_calc || null)
|
| 101 |
+
setPlotResiduos(resp.grafico_residuos || null)
|
| 102 |
+
setPlotHistograma(resp.grafico_histograma || null)
|
| 103 |
+
setPlotCook(resp.grafico_cook || null)
|
| 104 |
+
setPlotCorr(resp.grafico_correlacao || null)
|
| 105 |
+
|
| 106 |
+
setMapaHtml(resp.mapa_html || '')
|
| 107 |
+
setMapaChoices(resp.mapa_choices || ['Visualização Padrão'])
|
| 108 |
+
setMapaVar('Visualização Padrão')
|
| 109 |
+
|
| 110 |
+
setCamposAvaliacao(resp.campos_avaliacao || [])
|
| 111 |
+
const values = {}
|
| 112 |
+
;(resp.campos_avaliacao || []).forEach((campo) => {
|
| 113 |
+
values[campo.coluna] = ''
|
| 114 |
+
})
|
| 115 |
+
setValoresAvaliacao(values)
|
| 116 |
+
setResultadoAvaliacaoHtml('')
|
| 117 |
+
setBaseChoices([])
|
| 118 |
+
setBaseValue('')
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
async function withBusy(fn) {
|
| 122 |
+
setLoading(true)
|
| 123 |
+
setError('')
|
| 124 |
+
try {
|
| 125 |
+
await fn()
|
| 126 |
+
} catch (err) {
|
| 127 |
+
setError(err.message)
|
| 128 |
+
} finally {
|
| 129 |
+
setLoading(false)
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
async function onUploadModel() {
|
| 134 |
+
if (!sessionId || !uploadedFile) return
|
| 135 |
+
await withBusy(async () => {
|
| 136 |
+
resetConteudoVisualizacao()
|
| 137 |
+
const uploadResp = await api.uploadVisualizacaoFile(sessionId, uploadedFile)
|
| 138 |
+
setStatus(uploadResp.status || '')
|
| 139 |
+
setBadgeHtml(uploadResp.badge_html || '')
|
| 140 |
+
|
| 141 |
+
const exibirResp = await api.exibirVisualizacao(sessionId)
|
| 142 |
+
applyExibicaoResponse(exibirResp)
|
| 143 |
+
})
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
async function onMapChange(value) {
|
| 147 |
+
setMapaVar(value)
|
| 148 |
+
if (!sessionId) return
|
| 149 |
+
await withBusy(async () => {
|
| 150 |
+
const resp = await api.updateVisualizacaoMap(sessionId, value)
|
| 151 |
+
setMapaHtml(resp.mapa_html || '')
|
| 152 |
+
})
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
async function onCalcularAvaliacao() {
|
| 156 |
+
if (!sessionId) return
|
| 157 |
+
await withBusy(async () => {
|
| 158 |
+
const resp = await api.evaluationCalculateViz(sessionId, valoresAvaliacao, baseValue || null)
|
| 159 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 160 |
+
setBaseChoices(resp.base_choices || [])
|
| 161 |
+
setBaseValue(resp.base_value || '')
|
| 162 |
+
})
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
async function onClearAvaliacao() {
|
| 166 |
+
if (!sessionId) return
|
| 167 |
+
await withBusy(async () => {
|
| 168 |
+
const resp = await api.evaluationClearViz(sessionId)
|
| 169 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 170 |
+
setBaseChoices(resp.base_choices || [])
|
| 171 |
+
setBaseValue(resp.base_value || '')
|
| 172 |
+
})
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
async function onDeleteAvaliacao() {
|
| 176 |
+
if (!sessionId) return
|
| 177 |
+
await withBusy(async () => {
|
| 178 |
+
const resp = await api.evaluationDeleteViz(sessionId, deleteAvalIndex, baseValue || null)
|
| 179 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 180 |
+
setBaseChoices(resp.base_choices || [])
|
| 181 |
+
setBaseValue(resp.base_value || '')
|
| 182 |
+
setDeleteAvalIndex('')
|
| 183 |
+
})
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
async function onBaseChange(value) {
|
| 187 |
+
setBaseValue(value)
|
| 188 |
+
if (!sessionId) return
|
| 189 |
+
await withBusy(async () => {
|
| 190 |
+
const resp = await api.evaluationBaseViz(sessionId, value)
|
| 191 |
+
setResultadoAvaliacaoHtml(resp.resultado_html || '')
|
| 192 |
+
})
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
async function onExportAvaliacoes() {
|
| 196 |
+
if (!sessionId) return
|
| 197 |
+
await withBusy(async () => {
|
| 198 |
+
const blob = await api.exportEvaluationViz(sessionId)
|
| 199 |
+
downloadBlob(blob, 'avaliacoes_visualizacao.xlsx')
|
| 200 |
+
})
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
return (
|
| 204 |
+
<div className="tab-content">
|
| 205 |
+
<div className="status-strip">
|
| 206 |
+
{statusFluxo.map((item) => (
|
| 207 |
+
<span key={item.label} className={item.done ? 'status-pill done' : 'status-pill'}>
|
| 208 |
+
{item.label}
|
| 209 |
+
</span>
|
| 210 |
+
))}
|
| 211 |
+
</div>
|
| 212 |
+
|
| 213 |
+
<SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
|
| 214 |
+
<div className="row">
|
| 215 |
+
<input type="file" onChange={(e) => setUploadedFile(e.target.files?.[0] ?? null)} />
|
| 216 |
+
<button onClick={onUploadModel} disabled={!uploadedFile || loading || !sessionId}>Carregar Modelo</button>
|
| 217 |
+
</div>
|
| 218 |
+
{status ? <div className="status-line">{status}</div> : null}
|
| 219 |
+
{badgeHtml ? <div dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
|
| 220 |
+
</SectionBlock>
|
| 221 |
+
|
| 222 |
+
{dados ? (
|
| 223 |
+
<SectionBlock step="2" title="Conteúdo do Modelo" subtitle="Carregue o modelo no topo e navegue pelas abas internas abaixo.">
|
| 224 |
+
<div className="inner-tabs" role="tablist" aria-label="Abas internas de visualização">
|
| 225 |
+
{INNER_TABS.map((tab) => (
|
| 226 |
+
<button
|
| 227 |
+
key={tab.key}
|
| 228 |
+
type="button"
|
| 229 |
+
className={activeInnerTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 230 |
+
onClick={() => setActiveInnerTab(tab.key)}
|
| 231 |
+
>
|
| 232 |
+
{tab.label}
|
| 233 |
+
</button>
|
| 234 |
+
))}
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
<div className="inner-tab-panel">
|
| 238 |
+
{activeInnerTab === 'mapa' ? (
|
| 239 |
+
<>
|
| 240 |
+
<div className="row compact">
|
| 241 |
+
<label>Variável no mapa</label>
|
| 242 |
+
<select value={mapaVar} onChange={(e) => onMapChange(e.target.value)}>
|
| 243 |
+
{mapaChoices.map((choice) => (
|
| 244 |
+
<option key={choice} value={choice}>{choice}</option>
|
| 245 |
+
))}
|
| 246 |
+
</select>
|
| 247 |
+
</div>
|
| 248 |
+
<MapFrame html={mapaHtml} />
|
| 249 |
+
</>
|
| 250 |
+
) : null}
|
| 251 |
+
|
| 252 |
+
{activeInnerTab === 'dados' ? (
|
| 253 |
+
<div className="two-col">
|
| 254 |
+
<div className="pane">
|
| 255 |
+
<h4>Dados</h4>
|
| 256 |
+
<DataTable table={dados} maxHeight={520} />
|
| 257 |
+
</div>
|
| 258 |
+
<div className="pane">
|
| 259 |
+
<h4>Estatísticas</h4>
|
| 260 |
+
<DataTable table={estatisticas} maxHeight={520} />
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
+
) : null}
|
| 264 |
+
|
| 265 |
+
{activeInnerTab === 'transformacoes' ? (
|
| 266 |
+
<>
|
| 267 |
+
<div dangerouslySetInnerHTML={{ __html: escalasHtml }} />
|
| 268 |
+
<DataTable table={dadosTransformados} />
|
| 269 |
+
</>
|
| 270 |
+
) : null}
|
| 271 |
+
|
| 272 |
+
{activeInnerTab === 'resumo' ? (
|
| 273 |
+
<div dangerouslySetInnerHTML={{ __html: resumoHtml }} />
|
| 274 |
+
) : null}
|
| 275 |
+
|
| 276 |
+
{activeInnerTab === 'coeficientes' ? (
|
| 277 |
+
<DataTable table={coeficientes} maxHeight={620} />
|
| 278 |
+
) : null}
|
| 279 |
+
|
| 280 |
+
{activeInnerTab === 'obs_calc' ? (
|
| 281 |
+
<DataTable table={obsCalc} maxHeight={620} />
|
| 282 |
+
) : null}
|
| 283 |
+
|
| 284 |
+
{activeInnerTab === 'graficos' ? (
|
| 285 |
+
<>
|
| 286 |
+
<div className="plot-grid-2-fixed">
|
| 287 |
+
<PlotFigure figure={plotObsCalc} title="Obs x Calc" />
|
| 288 |
+
<PlotFigure figure={plotResiduos} title="Resíduos" />
|
| 289 |
+
<PlotFigure figure={plotHistograma} title="Histograma" />
|
| 290 |
+
<PlotFigure figure={plotCook} title="Cook" forceHideLegend />
|
| 291 |
+
</div>
|
| 292 |
+
<div className="plot-full-width">
|
| 293 |
+
<PlotFigure figure={plotCorr} title="Correlação" className="plot-correlation-card" />
|
| 294 |
+
</div>
|
| 295 |
+
</>
|
| 296 |
+
) : null}
|
| 297 |
+
|
| 298 |
+
{activeInnerTab === 'avaliacao' ? (
|
| 299 |
+
<>
|
| 300 |
+
<div className="avaliacao-grid">
|
| 301 |
+
{camposAvaliacao.map((campo) => (
|
| 302 |
+
<div key={`campo-${campo.coluna}`} className="avaliacao-card">
|
| 303 |
+
<label>{campo.coluna}</label>
|
| 304 |
+
<input
|
| 305 |
+
type="number"
|
| 306 |
+
value={valoresAvaliacao[campo.coluna] ?? ''}
|
| 307 |
+
placeholder={campo.placeholder || ''}
|
| 308 |
+
onChange={(e) => setValoresAvaliacao((prev) => ({ ...prev, [campo.coluna]: e.target.value }))}
|
| 309 |
+
/>
|
| 310 |
+
</div>
|
| 311 |
+
))}
|
| 312 |
+
</div>
|
| 313 |
+
|
| 314 |
+
<div className="row-wrap avaliacao-actions-row">
|
| 315 |
+
<button onClick={onCalcularAvaliacao} disabled={loading}>Calcular</button>
|
| 316 |
+
<button onClick={onClearAvaliacao} disabled={loading}>Limpar</button>
|
| 317 |
+
<button onClick={onExportAvaliacoes} disabled={loading}>Exportar avaliações</button>
|
| 318 |
+
</div>
|
| 319 |
+
|
| 320 |
+
<div className="row avaliacao-base-row">
|
| 321 |
+
<label>Base comparação</label>
|
| 322 |
+
<select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
|
| 323 |
+
<option value="">Selecione</option>
|
| 324 |
+
{baseChoices.map((choice) => (
|
| 325 |
+
<option key={`base-${choice}`} value={choice}>{choice}</option>
|
| 326 |
+
))}
|
| 327 |
+
</select>
|
| 328 |
+
|
| 329 |
+
<label>Excluir avaliação #</label>
|
| 330 |
+
<input type="text" value={deleteAvalIndex} onChange={(e) => setDeleteAvalIndex(e.target.value)} />
|
| 331 |
+
<button onClick={onDeleteAvaliacao} disabled={loading}>Excluir</button>
|
| 332 |
+
</div>
|
| 333 |
+
|
| 334 |
+
<div className="avaliacao-resultado-box" dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }} />
|
| 335 |
+
</>
|
| 336 |
+
) : null}
|
| 337 |
+
|
| 338 |
+
{activeInnerTab === 'avaliacao_massa' ? (
|
| 339 |
+
<div className="empty-box">Módulo em desenvolvimento.</div>
|
| 340 |
+
) : null}
|
| 341 |
+
</div>
|
| 342 |
+
</SectionBlock>
|
| 343 |
+
) : (
|
| 344 |
+
<div className="empty-box">Carregue um modelo para navegar nas abas internas da Visualização/Avaliação.</div>
|
| 345 |
+
)}
|
| 346 |
+
|
| 347 |
+
{loading ? <div className="status-line">Processando...</div> : null}
|
| 348 |
+
{error ? <div className="error-line">{error}</div> : null}
|
| 349 |
+
</div>
|
| 350 |
+
)
|
| 351 |
+
}
|