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() # Criar um range de datas para todo o ano de 2025 para contabilizar os dias sem voo import itertools datas_2025 = pd.date_range(start='2025-01-01', end='2025-12-31').date todos_trechos = demanda_diaria['Trecho'].unique() idx = pd.MultiIndex.from_product([todos_trechos, datas_2025], names=['Trecho', 'Data Apenas']) demanda_diaria_completa = pd.DataFrame(index=idx).reset_index() # Mesclar com os dados reais e preencher NaN com 0 demanda_diaria_completa = pd.merge(demanda_diaria_completa, demanda_diaria, on=['Trecho', 'Data Apenas'], how='left').fillna({'PAX': 0}) # Estatísticas (Média e Desvio Padrão) por trecho estatisticas_trechos = demanda_diaria_completa.groupby('Trecho').agg( Media_PAX_Diario=('PAX', 'mean'), Desvio_Padrao_PAX=('PAX', 'std'), Total_PAX=('PAX', 'sum'), Dias_Operados=('PAX', lambda x: (x > 0).sum()) ).reset_index() # Média PAX por dias operados (usado para o Fleet Assignment Model) estatisticas_trechos['Media_PAX_Dias_Operados'] = estatisticas_trechos.apply( lambda row: row['Total_PAX'] / row['Dias_Operados'] if row['Dias_Operados'] > 0 else 0, axis=1 ) # Nova métrica solicitada: PAX * Dias_Operados estatisticas_trechos['Score_Relevancia'] = estatisticas_trechos['Total_PAX'] * estatisticas_trechos['Dias_Operados'] # Ordenar pelas rotas com maior métrica (PAX * Dias_Operados) estatisticas_trechos = estatisticas_trechos.sort_values(by='Score_Relevancia', 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='Score_Relevancia', hue='Trecho', palette='viridis', legend=False) plt.title(f'Top {NUM_TRECHOS} Trechos Mais Relevantes (PAX * Dias Operados)') plt.ylabel('Score de Relevância (PAX * Dias Operados)') 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_Dias_Operados'] / 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!")