Initial project import
This commit is contained in:
257
scripts/07_validacao_2025.py
Normal file
257
scripts/07_validacao_2025.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""Validacao retrospectiva com voos reais de 2025.
|
||||
|
||||
Entradas:
|
||||
dados/Quadro de Voo 2025 (2).xlsx, aba VOOS, ou o CSV ja importado em
|
||||
dados/validacao/voos_2025.csv.
|
||||
|
||||
Saidas:
|
||||
resultados/validacao/validacao_2025_resumo.xlsx
|
||||
resultados/validacao/validacao_2025_detalhada.xlsx
|
||||
resultados/validacao/validacao_2025_metricas.csv
|
||||
resultados/validacao/validacao_2025_barras.png
|
||||
|
||||
Papel no pipeline:
|
||||
Mantem a demanda historica de 2025, gera candidatos compativeis para cada
|
||||
slot historico e usa MILP para redistribuir tripulantes preservando horas.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import re
|
||||
import struct
|
||||
import zlib
|
||||
from datetime import time, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
config = importlib.import_module("01_config")
|
||||
io_utils = importlib.import_module("02_io_utils")
|
||||
|
||||
COLUNAS_VALIDACAO = ["slot_id", "data", "aeronave", "tipo_escala", "tripulante", "funcao", "oi", "horas_voadas", "sbv"]
|
||||
|
||||
|
||||
def _normalizar_texto(valor: object) -> str:
|
||||
from src.planejador_missao.utils import normalizar_texto
|
||||
|
||||
return normalizar_texto(valor)
|
||||
|
||||
|
||||
def _horas(valor: object) -> float:
|
||||
"""Converte TEV da planilha historica para horas decimais."""
|
||||
if pd.isna(valor):
|
||||
return 0.0
|
||||
if isinstance(valor, timedelta):
|
||||
return round(valor.total_seconds() / 3600, 2)
|
||||
if isinstance(valor, time):
|
||||
return round(valor.hour + valor.minute / 60 + valor.second / 3600, 2)
|
||||
if isinstance(valor, str):
|
||||
texto = valor.strip()
|
||||
if not texto or texto == "-":
|
||||
return 0.0
|
||||
partes = texto.split(":")
|
||||
if len(partes) >= 2:
|
||||
return round(float(partes[0]) + float(partes[1]) / 60, 2)
|
||||
return float(texto.replace(",", "."))
|
||||
if isinstance(valor, (int, float)):
|
||||
return round(float(valor) * 24, 2) if 0 < float(valor) < 1 else float(valor)
|
||||
return 0.0
|
||||
|
||||
|
||||
def _aeronave(valor: object) -> str:
|
||||
"""Traduz matricula FAB para familia operacional C95/C97/C98."""
|
||||
texto = _normalizar_texto(valor)
|
||||
match = re.search(r"(\d{4})", texto)
|
||||
if not match:
|
||||
return ""
|
||||
numero = match.group(1)
|
||||
if numero.startswith("20"):
|
||||
return "C97"
|
||||
if numero.startswith("23"):
|
||||
return "C95"
|
||||
if numero.startswith("27"):
|
||||
return "C98"
|
||||
return ""
|
||||
|
||||
|
||||
def _tripulantes(valor: object) -> list[str]:
|
||||
"""Extrai a dupla principal da tripulacao historica."""
|
||||
partes = re.split(r"\s*/\s*", str(valor))
|
||||
trips = []
|
||||
for parte in partes:
|
||||
texto = _normalizar_texto(parte)
|
||||
texto = re.sub(r"\([^)]*\)", " ", texto)
|
||||
texto = re.sub(r"[^A-Z0-9]+", " ", texto).strip()
|
||||
if texto and texto != "NAN":
|
||||
trips.append(texto)
|
||||
return trips[:2]
|
||||
|
||||
|
||||
def importar_voos_2025(origem: Path = config.ARQUIVO_QUADRO_VOO_2025, destino: Path = config.ARQUIVO_VOOS_2025) -> pd.DataFrame:
|
||||
"""Importa voos realizados de 2025 da aba VOOS para o CSV de validacao."""
|
||||
io_utils.exigir_arquivos([origem])
|
||||
raw = pd.read_excel(origem, sheet_name=config.ABAS["voos_2025"], header=None)
|
||||
linhas = []
|
||||
slot_id = 0
|
||||
for _, row in raw.iterrows():
|
||||
data = pd.to_datetime(row[2], errors="coerce")
|
||||
if pd.isna(data) or data.year != 2025:
|
||||
continue
|
||||
if _normalizar_texto(row[41]) != "REALIZADO":
|
||||
continue
|
||||
|
||||
aeronave = _aeronave(row[37])
|
||||
if not aeronave:
|
||||
continue
|
||||
|
||||
horas_voadas = _horas(row[30])
|
||||
if horas_voadas <= 0:
|
||||
continue
|
||||
|
||||
trips = _tripulantes(row[8])
|
||||
if len(trips) < 2:
|
||||
continue
|
||||
|
||||
slot_id += 1
|
||||
tipo_escala = _normalizar_texto(row[1])
|
||||
oi = _normalizar_texto(row[35])
|
||||
if not oi or oi == "-":
|
||||
oi = _normalizar_texto(row[32])
|
||||
|
||||
for idx, tripulante in enumerate(trips, start=1):
|
||||
linhas.append(
|
||||
{
|
||||
"slot_id": slot_id,
|
||||
"data": data.date().isoformat(),
|
||||
"aeronave": aeronave,
|
||||
"tipo_escala": tipo_escala,
|
||||
"tripulante": tripulante,
|
||||
"funcao": "PILOTO" if idx == 1 else "COPILOTO",
|
||||
"oi": oi,
|
||||
"horas_voadas": horas_voadas,
|
||||
"sbv": 0,
|
||||
}
|
||||
)
|
||||
|
||||
df = pd.DataFrame(linhas, columns=COLUNAS_VALIDACAO)
|
||||
io_utils.exportar_csv(destino, df)
|
||||
return df
|
||||
|
||||
|
||||
def _png_barras(path: Path, metricas: pd.DataFrame) -> Path:
|
||||
"""Gera PNG simples sem depender de bibliotecas graficas externas."""
|
||||
width, height = 760, 420
|
||||
canvas = bytearray([255, 255, 255] * width * height)
|
||||
|
||||
def rect(x0: int, y0: int, x1: int, y1: int, color: tuple[int, int, int]) -> None:
|
||||
x0, x1 = max(0, x0), min(width, x1)
|
||||
y0, y1 = max(0, y0), min(height, y1)
|
||||
for y in range(y0, y1):
|
||||
for x in range(x0, x1):
|
||||
i = (y * width + x) * 3
|
||||
canvas[i : i + 3] = bytes(color)
|
||||
|
||||
rect(55, 45, 60, 365, (38, 57, 77))
|
||||
rect(55, 360, 705, 365, (38, 57, 77))
|
||||
valores = metricas.set_index("cenario")["desvio_padrao"].to_dict()
|
||||
max_v = max(valores.values()) if valores else 1
|
||||
cores = {"real_2025": (154, 52, 18), "otimizado_meta_50": (38, 115, 77)}
|
||||
xs = {"real_2025": 190, "otimizado_meta_50": 430}
|
||||
for cenario, valor in valores.items():
|
||||
h = int((valor / max_v) * 270)
|
||||
rect(xs[cenario], 360 - h, xs[cenario] + 110, 360, cores[cenario])
|
||||
|
||||
raw = b"".join(b"\x00" + canvas[y * width * 3 : (y + 1) * width * 3] for y in range(height))
|
||||
png = b"\x89PNG\r\n\x1a\n"
|
||||
for chunk_type, data in [
|
||||
(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)),
|
||||
(b"IDAT", zlib.compress(raw, 9)),
|
||||
(b"IEND", b""),
|
||||
]:
|
||||
png += struct.pack(">I", len(data)) + chunk_type + data + struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(png)
|
||||
return path
|
||||
|
||||
|
||||
def executar_validacao_2025(importar: bool = True, logger=None) -> dict:
|
||||
"""Executa a validacao completa e exporta os artefatos finais."""
|
||||
io_utils.configurar_ambiente()
|
||||
logger = logger or io_utils.configurar_logger("validacao_2025")
|
||||
inicio = pd.Timestamp.now()
|
||||
|
||||
if importar or not config.ARQUIVO_VOOS_2025.exists():
|
||||
voos_importados = importar_voos_2025()
|
||||
logger.info("Voos 2025 importados: %s registros", len(voos_importados))
|
||||
|
||||
from src.planejador_missao.validacao import carregar_voos_2025, otimizar_equalizacao_50
|
||||
|
||||
voos = carregar_voos_2025(config.ARQUIVO_VOOS_2025)
|
||||
if voos.empty:
|
||||
raise RuntimeError("Erro: base de validacao 2025 vazia. Verifique dados/validacao/voos_2025.csv.")
|
||||
if (voos["horas_voadas"] < 0).any():
|
||||
raise RuntimeError("Erro: foram encontradas horas voadas negativas na validacao 2025.")
|
||||
|
||||
resultado = otimizar_equalizacao_50(
|
||||
config.PROJECT_ROOT,
|
||||
voos,
|
||||
meta_horas=config.META_HORAS_PADRAO,
|
||||
tempo_limite_segundos=config.VALIDACAO_TEMPO_LIMITE_SEGUNDOS,
|
||||
gap_relativo=config.VALIDACAO_GAP_RELATIVO,
|
||||
)
|
||||
metricas = resultado["metricas"]
|
||||
total_real = float(metricas.loc[metricas["cenario"] == "real_2025", "horas_total"].iloc[0])
|
||||
total_otim = float(metricas.loc[metricas["cenario"] == "otimizado_meta_50", "horas_total"].iloc[0])
|
||||
if abs(total_real - total_otim) > 0.01:
|
||||
logger.warning("Horas nao preservadas: real=%s otimizado=%s", total_real, total_otim)
|
||||
|
||||
resumo_path = config.VALIDACAO_DIR / "validacao_2025_resumo.xlsx"
|
||||
detalhada_path = config.VALIDACAO_DIR / "validacao_2025_detalhada.xlsx"
|
||||
metricas_path = config.VALIDACAO_DIR / "validacao_2025_metricas.csv"
|
||||
grafico_path = config.VALIDACAO_DIR / "validacao_2025_barras.png"
|
||||
|
||||
solver_df = pd.DataFrame([resultado.get("status_solver", {})])
|
||||
io_utils.exportar_excel(resumo_path, {"solver": solver_df, "metricas": metricas})
|
||||
io_utils.exportar_excel(
|
||||
detalhada_path,
|
||||
{
|
||||
"solver": solver_df,
|
||||
"metricas": metricas,
|
||||
"comparativo_trips": resultado["comparativo"],
|
||||
"voos_2025_slots": resultado["slots"],
|
||||
"escala_otimizada": resultado["otimizados"],
|
||||
},
|
||||
)
|
||||
io_utils.exportar_csv(metricas_path, metricas)
|
||||
_png_barras(grafico_path, metricas)
|
||||
|
||||
duracao = (pd.Timestamp.now() - inicio).total_seconds()
|
||||
logger.info("Slots historicos avaliados: %s", len(resultado["slots"]))
|
||||
logger.info("Alocacoes otimizadas: %s", len(resultado["otimizados"]))
|
||||
logger.info("Tripulantes comparados: %s", len(resultado["comparativo"]))
|
||||
logger.info("Status solver: %s", resultado["status_solver"]["mensagem"])
|
||||
logger.info("Tempo de validacao: %.1f s", duracao)
|
||||
logger.info("Arquivos exportados: %s | %s | %s | %s", resumo_path, detalhada_path, metricas_path, grafico_path)
|
||||
|
||||
return {
|
||||
"resultado": resultado,
|
||||
"arquivos": [resumo_path, detalhada_path, metricas_path, grafico_path],
|
||||
"duracao_segundos": duracao,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Ponto de entrada da validacao 2025."""
|
||||
logger = io_utils.configurar_logger("validacao_2025")
|
||||
saida = executar_validacao_2025(importar=True, logger=logger)
|
||||
metricas = saida["resultado"]["metricas"]
|
||||
print("\n=== Validacao 2025 concluida ===")
|
||||
print(metricas.to_string(index=False))
|
||||
print("\nArquivos gerados:")
|
||||
for path in saida["arquivos"]:
|
||||
print(f"- {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user