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 import networkx as nx # 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: Markdown Diagrama de Rede cells.append(nbf.v4.new_markdown_cell("""## Diagrama de Rede dos Trechos Para melhor visualização dos trechos mais relevantes, abaixo temos um diagrama de rede conectando as origens aos destinos. """)) # Cell: Code Diagrama de Rede cells.append(nbf.v4.new_code_cell("""# Criação do grafo direcionado G = nx.DiGraph() # Adiciona arestas com peso baseado no Score de Relevância for _, row in top_trechos.iterrows(): origem, destino = row['Trecho'].split('-') peso = row['Score_Relevancia'] G.add_edge(origem, destino, weight=peso) # Configuração do layout do grafo pos = nx.spring_layout(G, seed=42) plt.figure(figsize=(8, 6)) # Define o tamanho do nó tamanho_no = 1500 # Desenha os nós nx.draw_networkx_nodes(G, pos, node_size=tamanho_no, node_color='skyblue', edgecolors='black') # Desenha as arestas, informando o node_size para que a seta não fique escondida sob o nó nx.draw_networkx_edges( G, pos, edgelist=G.edges(), arrows=True, arrowstyle='-|>', arrowsize=25, edge_color='gray', width=2, node_size=tamanho_no, connectionstyle='arc3,rad=0.15' ) # Desenha os rótulos (nomes dos aeroportos) nx.draw_networkx_labels(G, pos, font_size=12, font_family='sans-serif', font_weight='bold') plt.title('Diagrama de Rede - Top Trechos Relevantes', fontsize=14) plt.axis('off') 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!")