226 lines
9.3 KiB
Python
226 lines
9.3 KiB
Python
import nbformat as nbf
|
|
|
|
nb = nbf.v4.new_notebook()
|
|
|
|
cells = []
|
|
|
|
# Cell 1: Markdown Introduction
|
|
cells.append(nbf.v4.new_markdown_cell("""# Alocação de Frotas (Fleet Scheduling) - Aeronaves C-97
|
|
|
|
Este projeto visa realizar a alocação de frota (Fleet Assignment) baseada nos princípios do livro referenciado *Airline Operations and Scheduling*.
|
|
Diferentemente do modelo anterior que utilizava aeronaves de carga, este foca exclusivamente na aeronave de passageiros **C-97**.
|
|
|
|
## Objetivos:
|
|
1. Remover cálculos de aeronaves de carga.
|
|
2. Analisar as demandas diárias para a aeronave C-97.
|
|
3. Identificar os esquadrões detentores dessas aeronaves e a quantidade de aeronaves (matrículas únicas).
|
|
4. Calcular os 5 trechos mais relevantes do ano de 2025 utilizando estatística e gráficos.
|
|
5. Realizar o Fleet Assignment para os 5 trechos, visando minimizar o custo total da operação (Modelo 1 proposto).
|
|
"""))
|
|
|
|
# Cell 2: Imports
|
|
cells.append(nbf.v4.new_code_cell("""# Importação de bibliotecas
|
|
import pandas as pd
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
import seaborn as sns
|
|
import pulp
|
|
import pymap3d as pm
|
|
import math
|
|
|
|
# Configuração de estilo para os gráficos
|
|
sns.set_theme(style="whitegrid")
|
|
|
|
# Constantes globais
|
|
CSV_FILEPATH = "SISCO_AEREA_01-01-2025_A_31-12-2025_1911251714Z.csv"
|
|
JSON_AEROPORTOS = "airports.json"
|
|
CAPACIDADE_C97 = 30 # Capacidade média de passageiros do C-97 (Brasília)
|
|
"""))
|
|
|
|
# Cell 3: Markdown Load Data
|
|
cells.append(nbf.v4.new_markdown_cell("""## 1. Leitura e Limpeza dos Dados
|
|
Vamos carregar os dados, filtrar apenas para o modelo `C-97` e tratar os campos de localidades e datas.
|
|
"""))
|
|
|
|
# Cell 4: Code Load Data
|
|
cells.append(nbf.v4.new_code_cell("""# Carregamento do banco de dados
|
|
df = pd.read_csv(CSV_FILEPATH, low_memory=False)
|
|
df_aeroportos = pd.read_json(JSON_AEROPORTOS, orient='index')
|
|
|
|
# Tratamento básico de valores nulos expressos como 'NIL'
|
|
df.replace('NIL', 0, inplace=True)
|
|
|
|
# Filtro para a aeronave C-97 (Passageiros) e remoção de registros de carga
|
|
df_c97 = df[df['Modelo'] == 'C-97'].copy()
|
|
|
|
# Filtrando decolagens e pousos conhecidos
|
|
df_c97 = df_c97[df_c97['Localidade Decolagem'] != 0]
|
|
df_c97 = df_c97[df_c97['Localidade Pouso'] != 0]
|
|
|
|
# Criação da coluna Trecho
|
|
df_c97['Trecho'] = df_c97['Localidade Decolagem'] + '-' + df_c97['Localidade Pouso']
|
|
|
|
# Conversão da coluna PAX para numérico
|
|
df_c97['PAX'] = pd.to_numeric(df_c97['PAX']).fillna(0)
|
|
|
|
# Tratamento de datas
|
|
df_c97['Dep TimeStamp'] = pd.to_datetime(df_c97['Data de Decolagem'] + ' ' + df_c97['Hora de Decolagem'], format='mixed', dayfirst=True)
|
|
df_c97['Data Apenas'] = df_c97['Dep TimeStamp'].dt.date
|
|
"""))
|
|
|
|
# Cell 5: Markdown Squadrons
|
|
cells.append(nbf.v4.new_markdown_cell("""## 2. Análise dos Esquadrões e Matrículas (Aeronaves)
|
|
Identificaremos quais esquadrões operam o C-97 e qual a disponibilidade de aeronaves (número de matrículas únicas), pois isso definirá as restrições de frota.
|
|
"""))
|
|
|
|
# Cell 6: Code Squadrons
|
|
cells.append(nbf.v4.new_code_cell("""# Obtendo esquadrões e matrículas únicas
|
|
esquadroes = df_c97['Emissor'].unique()
|
|
matriculas_unicas = df_c97['Matrícula'].unique()
|
|
total_aeronaves = len(matriculas_unicas)
|
|
|
|
print(f"Esquadrões que operam o C-97: {', '.join(esquadroes)}")
|
|
print(f"Total de matrículas únicas (aeronaves disponíveis): {total_aeronaves}")
|
|
|
|
# Aeronaves por esquadrão
|
|
aeronaves_por_esquadrao = df_c97.groupby('Emissor')['Matrícula'].nunique().to_dict()
|
|
print("\\nAeronaves por esquadrão:")
|
|
for esq, qtd in aeronaves_por_esquadrao.items():
|
|
print(f" - {esq}: {qtd} aeronaves")
|
|
|
|
# Identificar a base principal de cada esquadrão (aeroporto com mais decolagens)
|
|
bases_esquadroes = {}
|
|
for esq in aeronaves_por_esquadrao.keys():
|
|
base = df_c97[df_c97['Emissor'] == esq]['Localidade Decolagem'].mode()[0]
|
|
bases_esquadroes[esq] = base
|
|
|
|
print("\\nBases inferidas por esquadrão (pela moda de decolagens):")
|
|
for esq, base in bases_esquadroes.items():
|
|
print(f" - {esq}: {base}")
|
|
"""))
|
|
|
|
# Cell 7: Markdown Stats
|
|
cells.append(nbf.v4.new_markdown_cell("""## 3. Análise Estatística das Demandas Diárias
|
|
Para definir os trechos mais relevantes do ano, calcularemos a demanda de passageiros (PAX) agregada diariamente. Em seguida, extraímos a **média de passageiros diários** e o **desvio padrão** para cada trecho. A relevância será determinada pela maior média de demanda diária de PAX.
|
|
Começaremos focando em 5 trechos (alvo variável).
|
|
"""))
|
|
|
|
# Cell 8: Code Stats
|
|
cells.append(nbf.v4.new_code_cell("""# Demanda diária por trecho
|
|
demanda_diaria = df_c97.groupby(['Trecho', 'Data Apenas'])['PAX'].sum().reset_index()
|
|
|
|
# Estatísticas (Média e Desvio Padrão) por trecho
|
|
estatisticas_trechos = demanda_diaria.groupby('Trecho').agg(
|
|
Media_PAX_Diario=('PAX', 'mean'),
|
|
Desvio_Padrao_PAX=('PAX', 'std'),
|
|
Total_PAX=('PAX', 'sum'),
|
|
Dias_Operados=('Data Apenas', 'count')
|
|
).reset_index()
|
|
|
|
# Preencher NaN no desvio padrão (caso operado apenas 1 dia) com 0
|
|
estatisticas_trechos['Desvio_Padrao_PAX'] = estatisticas_trechos['Desvio_Padrao_PAX'].fillna(0)
|
|
|
|
# Ordenar pelas rotas de maior média de PAX diário
|
|
estatisticas_trechos = estatisticas_trechos.sort_values(by='Media_PAX_Diario', ascending=False)
|
|
|
|
# Definir número de trechos alvo
|
|
NUM_TRECHOS = 5
|
|
top_trechos = estatisticas_trechos.head(NUM_TRECHOS)
|
|
|
|
display(top_trechos)
|
|
|
|
# Gráfico
|
|
plt.figure(figsize=(10, 6))
|
|
sns.barplot(data=top_trechos, x='Trecho', y='Media_PAX_Diario', hue='Trecho', palette='viridis', legend=False)
|
|
plt.title(f'Top {NUM_TRECHOS} Trechos Mais Relevantes (Média de PAX Diário)')
|
|
plt.ylabel('Média Diária de Passageiros')
|
|
plt.xlabel('Trecho (Origem - Destino)')
|
|
plt.xticks(rotation=45)
|
|
plt.tight_layout()
|
|
plt.show()
|
|
"""))
|
|
|
|
# Cell 9: Markdown FAM
|
|
cells.append(nbf.v4.new_markdown_cell("""## 4. Fleet Assignment Model (FAM)
|
|
Conforme o livro base *Airline Operations and Scheduling*, o problema de alocação de frotas busca atribuir os tipos de aeronaves às pernas de voo visando minimizar o custo e as perdas de receita (spill). Como temos apenas um tipo (C-97), nosso problema adapta-se a uma **alocação de esquadrões (e suas respectivas bases)** aos voos requeridos para minimizar o custo total de operação, que é proporcional à distância voada.
|
|
|
|
**Modelo Matemático:**
|
|
- **Variáveis de decisão:** $X_{s, r}$ (Quantidade de voos operados pelo esquadrão $s$ no trecho $r$)
|
|
- **Função Objetivo:** Minimizar a distância total (Distância de posicionamento da base + Distância do trecho + Retorno para a base).
|
|
- **Restrições:**
|
|
1. O número de voos alocados para o trecho deve suprir a demanda média (Média PAX / Capacidade do C-97).
|
|
2. O total de voos atribuídos a um esquadrão não pode exceder o número de aeronaves disponíveis naquele esquadrão no dia.
|
|
"""))
|
|
|
|
# Cell 10: Code FAM
|
|
cells.append(nbf.v4.new_code_cell("""# Preparação dos dados para o modelo
|
|
rotas_lista = top_trechos['Trecho'].tolist()
|
|
|
|
# Dicionário de distâncias
|
|
def calc_distancia(icao1, icao2):
|
|
try:
|
|
lat1, lon1, alt1 = df_aeroportos.loc[icao1, ['lat', 'lon', 'elevation']]
|
|
lat2, lon2, alt2 = df_aeroportos.loc[icao2, ['lat', 'lon', 'elevation']]
|
|
# Retorna a distância oblíqua em km
|
|
return pm.geodetic2aer(lat1, lon1, alt1, lat2, lon2, alt2)[2] / 1000.0
|
|
except KeyError:
|
|
return 9999.0 # Penalidade caso aeroporto não exista no json
|
|
|
|
distancias_voo = {}
|
|
for r in rotas_lista:
|
|
origem, destino = r.split('-')
|
|
dist_trecho = calc_distancia(origem, destino)
|
|
for esq, base in bases_esquadroes.items():
|
|
dist_ida = calc_distancia(base, origem) if base != origem else 0
|
|
dist_volta = calc_distancia(destino, base) if base != destino else 0
|
|
distancias_voo[(esq, r)] = dist_ida + dist_trecho + dist_volta
|
|
|
|
# Demanda de voos por rota (Média diária)
|
|
voos_requeridos = {}
|
|
for _, row in top_trechos.iterrows():
|
|
# Arredondamento para cima da (demanda / capacidade)
|
|
voos = math.ceil(row['Media_PAX_Diario'] / CAPACIDADE_C97)
|
|
# Pelo menos 1 voo para suprir a demanda da rota selecionada
|
|
voos_requeridos[row['Trecho']] = max(1, voos)
|
|
|
|
# MODELAGEM COM PULP
|
|
prob = pulp.LpProblem("Fleet_Assignment_C97", pulp.LpMinimize)
|
|
|
|
# Variáveis
|
|
X = pulp.LpVariable.dicts("X", [(esq, r) for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista], lowBound=0, cat='Integer')
|
|
|
|
# Função Objetivo
|
|
prob += pulp.lpSum([distancias_voo[(esq, r)] * X[(esq, r)] for esq in aeronaves_por_esquadrao.keys() for r in rotas_lista])
|
|
|
|
# Restrição 1: Suprir a demanda da rota
|
|
for r in rotas_lista:
|
|
prob += pulp.lpSum([X[(esq, r)] for esq in aeronaves_por_esquadrao.keys()]) >= voos_requeridos[r]
|
|
|
|
# Restrição 2: Limite de aeronaves por esquadrão
|
|
for esq in aeronaves_por_esquadrao.keys():
|
|
prob += pulp.lpSum([X[(esq, r)] for r in rotas_lista]) <= aeronaves_por_esquadrao[esq]
|
|
|
|
# Solução
|
|
prob.solve(pulp.PULP_CBC_CMD(msg=False))
|
|
|
|
# Exibição dos resultados
|
|
print("== RESULTADO DO FLEET ASSIGNMENT ==")
|
|
print(f"Status: {pulp.LpStatus[prob.status]}")
|
|
print(f"Distância Total Minimizada: {pulp.value(prob.objective):.2f} km\\n")
|
|
|
|
print("Alocações (Voos Diários):")
|
|
for r in rotas_lista:
|
|
print(f"\\nTrecho: {r} (Voos necessários: {voos_requeridos[r]})")
|
|
for esq in aeronaves_por_esquadrao.keys():
|
|
qtd = int(X[(esq, r)].varValue)
|
|
if qtd > 0:
|
|
print(f" -> {esq} (Base {bases_esquadroes[esq]}): {qtd} voo(s)")
|
|
"""))
|
|
|
|
nb.cells = cells
|
|
|
|
with open('modelos.ipynb', 'w', encoding='utf-8') as f:
|
|
nbf.write(nb, f)
|
|
|
|
print("modelos.ipynb gerado com sucesso!")
|