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 = 10 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 - k maior afasta mais os nós pos = nx.spring_layout(G, k=2.5, iterations=100, seed=42) plt.figure(figsize=(12, 8)) # Define o tamanho do nó tamanho_no = 1200 # 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) - Nível Brasil Conforme o MODELO 1 solicitado (aeronaves podem pernoitar em qualquer hub), não há restrição de base ou esquadrão específico. Toda a frota de C-97 opera a nível nacional. **Modelo Matemático:** - **Variáveis de decisão:** $X_r$ (Quantidade de voos alocados no trecho $r$) - **Função Objetivo:** Minimizar a distância total da operação (Distância do trecho $\\times$ voos no trecho). Como as aeronaves não precisam retornar a uma base fixa, desconsideramos voos de posicionamento. - **Restrições:** 1. O número de voos alocados para o trecho deve suprir a demanda diária operada daquela rota. 2. O total de voos atribuídos no dia não pode exceder o total da frota de C-97 disponível (14 aeronaves). """)) # 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 apenas dos trechos diretos 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('-') distancias_voo[r] = calc_distancia(origem, destino) # Demanda de voos por rota voos_requeridos = {} for _, row in top_trechos.iterrows(): voos = math.ceil(row['Media_PAX_Dias_Operados'] / CAPACIDADE_C97) voos_requeridos[row['Trecho']] = max(1, voos) # MODELAGEM COM PULP prob = pulp.LpProblem("Fleet_Assignment_C97_Brasil", pulp.LpMinimize) # Variáveis (número de voos em cada rota) X = pulp.LpVariable.dicts("X", rotas_lista, lowBound=0, cat='Integer') # Função Objetivo: Minimizar distância voada nos trechos prob += pulp.lpSum([distancias_voo[r] * X[r] for r in rotas_lista]) # Restrição 1: Suprir a demanda for r in rotas_lista: prob += X[r] >= voos_requeridos[r] # Restrição 2: Limite global de frota (nível Brasil) prob += pulp.lpSum([X[r] for r in rotas_lista]) <= total_aeronaves # Solução prob.solve(pulp.PULP_CBC_CMD(msg=False)) # Exibição dos resultados print("== RESULTADO DO FLEET ASSIGNMENT (NÍVEL BRASIL) ==") print(f"Status: {pulp.LpStatus[prob.status]}") print(f"Distância Total da Operação: {pulp.value(prob.objective):.2f} km\\n") print("Alocações (Voos Diários Globais):") voos_totais = 0 for r in rotas_lista: qtd = int(X[r].varValue) voos_totais += qtd print(f"Trecho: {r:10} | Demanda Média PAX: {voos_requeridos[r]*CAPACIDADE_C97:3.0f} | Alocados: {qtd} voo(s)") print(f"\\nTotal de voos alocados na malha diária: {voos_totais} de {total_aeronaves} aeronaves disponíveis.") """)) nb.cells = cells with open('modelos.ipynb', 'w', encoding='utf-8') as f: nbf.write(nb, f) print("modelos.ipynb gerado com sucesso!")