Previsão de Preços de Passagens Aéreas

Por Réulison Silva
Réulison Silva
Published on
Previsão de Preços de Passagens Aéreas com Machine Learning

1. Introdução e Contexto de Negócio

No competitivo setor de aviação comercial, a precificação dinâmica é um dos pilares da estratégia de revenue management. As companhias aéreas operam em um ambiente de alta volatilidade, onde fatores como sazonalidade, concorrência, custos operacionais e comportamento do consumidor se entrelaçam para definir o preço final de uma passagem.

Cenários Reais de Aplicação

Este projeto de previsão de preços pode ser aplicado em diversos contextos de negócio:

CenárioBenefícioExemplo Prático
Otimização de Receita (Revenue Management)Ajuste dinâmico de preços baseado em demanda previstaAntecipar picos de demanda em feriados e aumentar preços estrategicamente
Planejamento de Frota e RotasDecisões baseadas em dados sobre quais rotas são mais lucrativasIdentificar que voos para destinos turísticos têm maior elasticidade de preço
Marketing PersonalizadoOferecer descontos para perfis de passageiros com menor disposição a pagarEnviar ofertas específicas para passageiros que buscam rotas com baixa ocupação prevista
Gestão de CustosAlinhar preços com custos operacionais em tempo realPrever quedas de demanda e ajustar ofertas para manter ocupação mínima
Benchmarking CompetitivoMonitorar preços da concorrência e reagir rapidamenteAjustar preços em rotas onde concorrentes estão com promoções agressivas

2. Metodologia e Pipeline do Projeto

2.1 Fonte de Dados

O dataset airlines_flights_data.csv contém informações detalhadas sobre voos de diferentes companhias aéreas, incluindo:

  • airline: Companhia aérea (variável categórica)
  • duration: Duração do voo em minutos (variável numérica)
  • days_left: Dias restantes para a viagem (variável numérica)
  • price: Preço da passagem (target)
  • departure_time e arrival_time: Horários de partida e chegada (variáveis de data/hora)

2.2 Pipeline de Machine Learning

O pipeline implementado segue as melhores práticas de engenharia de machine learning:

Infográfico apresentando as sete etapas de um pipeline de Machine Learning: carregamento dos dados, análise exploratória, pré-processamento, engenharia de atributos (feature engineering), treinamento, avaliação e validação cruzada, com indicação de que o processo é iterativo para otimizar o desempenho do modelo
Infográfico apresentando as sete etapas de um pipeline de Machine Learning: carregamento dos dados, análise exploratória, pré-processamento, engenharia de atributos (feature engineering), treinamento, avaliação e validação cruzada, com indicação de que o processo é iterativo para otimizar o desempenho do modelo
Principais Etapas:
  1. Amostragem: Redução para 15.000 registros para balancear tempo de processamento e representatividade
  2. Tratamento de Datas: Conversão de departure_time e arrival_time para datetime
  3. Codificação de Variáveis Categóricas: Uso de OneHotEncoder para variáveis como airline
  4. Modelos Testados:
    • Random Forest Regressor: Conjunto de árvores de decisão para maior robustez
    • Decision Tree Regressor: Modelo mais simples para baseline

Random Forest Regressor: Uma Random Forest (floresta aleatória) é um metaestimador que ajusta vários regressores de árvore de decisão em diferentes subamostras do conjunto de dados e utiliza a média para melhorar a precisão preditiva e controlar o overfitting (sobreajuste). As árvores da floresta utilizam a estratégia de melhor divisão (best split), ou seja, o equivalente a definir splitter="best" no DecisionTreeRegressor subjacente. O tamanho da subamostra é controlado pelo parâmetro max_samples quando bootstrap=True (padrão); caso contrário, utiliza-se o conjunto de dados completo para construir cada árvore.

Decision Tree Regressor: A função para medir a qualidade de uma divisão. Os critérios suportados são squared_error para o erro quadrático médio — que equivale à redução da variância como critério de seleção de atributos e minimiza a perda L2 utilizando a média de cada nó terminal —, absolute_error para o erro absoluto médio — que minimiza a perda L1 utilizando a mediana de cada nó terminal — e poisson, que utiliza a redução da deviance de Poisson para encontrar divisões, também empregando a média de cada nó terminal.

3. Análise dos Resultados e Insights

3.1 Distribuição das Companhias Aéreas

A análise da distribuição de voos por companhia revela a concentração de mercado:

Gráfico de barras mostrando a contagem de voos por companhia aérea, com Vistara apresentando o maior número de voos, seguida por Air India, Indigo, GO FIRST, AirAsia e SpiceJet.
Gráfico de barras mostrando a contagem de voos por companhia aérea, com Vistara apresentando o maior número de voos, seguida por Air India, Indigo, GO FIRST, AirAsia e SpiceJet.

Insight de Negócio: A Vistara e a Air India dominam o mercado com aproximadamente 69% dos voos no dataset. Isso sugere que:

  • Estratégias de precificação nessas companhias terão maior impacto no negócio
  • Players menores (SpiceJet, AirAsia) podem usar preços competitivos para ganhar participação de mercado
  • A concentração de mercado indica a necessidade de monitoramento contínuo das estratégias dos líderes

3.2 Distribuição da Duração dos Voos

A análise da duração dos voos mostra a predominância de rotas curtas e regionais:

Gráfico de histograma representando a distribuição da duração dos voos em minutos, com barras indicando a frequência das observações e uma curva de densidade destacando a concentração de voos entre aproximadamente 5 e 15 minutos e uma cauda à direita para durações mais longas
Gráfico de histograma representando a distribuição da duração dos voos em minutos, com barras indicando a frequência das observações e uma curva de densidade destacando a concentração de voos entre aproximadamente 5 e 15 minutos e uma cauda à direita para durações mais longas

Insight de Negócio: A maioria dos voos tem duração entre 10-20 minutos, indicando rotas curtas e regionais. Isso é crucial para precificação, pois:

  • Voos curtos têm menor custo operacional por passageiro
  • Permitem margens maiores ou preços mais competitivos
  • A demanda por voos curtos tende a ser mais elástica a preço

3.3 Matriz de Correlação

A matriz de correlação entre as variáveis numéricas revela relações importantes:

Heatmap de correlação com três variáveis: duration, days_left e price. A diagonal principal apresenta valores 1.00 (correlação perfeita de cada variável consigo mesma). A correlação entre duration e price é 0.21 (positiva), entre days_left e price é -0.09 (negativa) e entre duration e days_left é -0.04 (praticamente nula). O mapa utiliza um gradiente de cores que vai do azul (correlações negativas) ao vermelho (correlações positivas), com intensidade proporcional ao valor.
Heatmap de correlação com três variáveis: duration, days_left e price. A diagonal principal apresenta valores 1.00 (correlação perfeita de cada variável consigo mesma). A correlação entre duration e price é 0.21 (positiva), entre days_left e price é -0.09 (negativa) e entre duration e days_left é -0.04 (praticamente nula). O mapa utiliza um gradiente de cores que vai do azul (correlações negativas) ao vermelho (correlações positivas), com intensidade proporcional ao valor.

Interpretação Estatística:

  • Correlação positiva (0.21) entre duration e price: Voos mais longos tendem a ser mais caros (esperado, devido ao maior consumo de combustível e custos operacionais)
  • Correlação negativa (-0.09) entre days_left e price: Comprar com mais antecedência pode resultar em preços ligeiramente menores (incentivo ao planejamento antecipado)
  • Correlação baixa entre variáveis: Não há multicolinearidade severa, indicando que as features são independentes e contribuem com informações únicas para o modelo

4. Desempenho dos Modelos

4.1 Métricas Comparativas

Os resultados da avaliação dos modelos são apresentados na tabela abaixo:

ModeloR² ScoreRMSE
Random Forest0.9644,344.82
Decision Tree0.9355,798.69

Tabela 1: Métricas de desempenho dos modelos avaliados.

4.2 Análise dos Resultados

Random Forest (Modelo Vencedor)
  • R² de 0.964: O modelo explica 96.4% da variância dos preços. Isso é excepcional para um problema de precificação, indicando que as features selecionadas capturam muito bem os padrões de preço.
  • RMSE de 4,344.82: Em média, o modelo erra o preço por cerca de R$ 4.345. Considerando que a média de preços no dataset é ~R$ 20.000, isso representa um erro relativo de ~21.7%.
  • Exemplo Prático: Um voo com preço real de R$ 42,457 teve previsão de R$ 42,876 (erro de apenas 1%). Para um voo de R$ 53,152, a previsão foi R$ 53,487.
Decision Tree (Baseline)
  • R² de 0.935: Ainda muito bom, mas inferior ao Random Forest.
  • RMSE de 5,798.69: Erro ~29% maior que o Random Forest, indicando menor capacidade de generalização.

4.3 Validação Cruzada (Random Forest)

Scores R² por fold:

Fold 1: 0.954
Fold 2: 0.961
Fold 3: 0.958
Fold 4: 0.953
Fold 5: 0.956
Média: 0.956 | Desvio Padrão: 0.003

Insight: A consistência dos scores (desvio padrão muito baixo) confirma que o modelo é estável e generalizável para novos dados. Isso é crucial para aplicação em produção, pois garante que a performance observada nos dados de treino se manterá em cenários reais.

5. Análise de Predições: Cases Específicos

5.1 Exemplo de Boa Predição (Random Forest)

VoosPreço RealPreço PrevistoErro
Voo AR$ 10,058R$ 11,029+9.6%
Voo BR$ 5,318R$ 5,086-4.4%
Voo CR$ 50,271R$ 50,239-0.06%

5.2 Outliers e Oportunidades de Ajuste

O modelo apresentou maiores erros em voos com preços muito baixos (ex: R$ 1,543 previsto como R$ 2,711) ou muito altos (ex: R$ 87,301 previsto como R$ 63,766). Isso sugere:

  1. Oportunidade: Voos de baixo custo podem ter fatores não capturados (ex: promoções relâmpago)
  2. Risco: Voos premium podem ter componentes de preço (ex: classe executiva, bagagem extra) não contemplados no dataset

5.3 Análise de Predições (Decision Tree)

A Decision Tree, embora menos precisa, mostra um padrão interessante em suas predições. Em muitos casos, o modelo tende a "memorizar" valores específicos, resultando em predições idênticas para diferentes registros. Isso é uma característica típica de árvores de decisão e explica sua menor capacidade de generalização.

6. Recomendações para Implantação em Produção

6.1 Arquitetura Sugerida

Infográfico intitulado Pipeline de Monitoramento de Preços com Machine Learning. O fluxo principal segue da esquerda para a direita: API de Preços → Modelo Random Forest → Dashboard de Monitoramento. Abaixo, há um fluxo de retorno: Banco de Dados ← Pipeline de Atualização ← Alertas de Drift. Uma seta liga a API de Preços ao Banco de Dados, e outra conecta os Alertas de Drift ao Dashboard, formando um ciclo contínuo de monitoramento e atualização.
Infográfico intitulado Pipeline de Monitoramento de Preços com Machine Learning. O fluxo principal segue da esquerda para a direita: API de Preços → Modelo Random Forest → Dashboard de Monitoramento. Abaixo, há um fluxo de retorno: Banco de Dados ← Pipeline de Atualização ← Alertas de Drift. Uma seta liga a API de Preços ao Banco de Dados, e outra conecta os Alertas de Drift ao Dashboard, formando um ciclo contínuo de monitoramento e atualização.

6.2 Estratégia de Implantação

FaseAçãoPrazo
PilotTeste com dados históricos de 1 mêsSemana 1-2
Shadow ModeExecução paralela sem interferir em decisõesSemana 3-4
Canary Deployment5% das requisições usando o novo modeloSemana 5-6
Full Production100% das requisições com monitoramento intensivoSemana 7+

6.3 Métricas de Monitoramento Contínuo

  • Performance: R² e RMSE recalculados semanalmente
  • Data Drift: Monitorar mudanças nas distribuições de duration e days_left
  • Concept Drift: Verificar se a relação entre features e preço muda ao longo do tempo
  • Latência: Tempo de resposta da API de predição

7. Impacto Estimado no Negócio

IndicadorAntesDepoisImpacto Estimado
Precisão de PrecificaçãoManual/ReativoPreditiva+15-20% em receita
Tempo de Resposta a Mercado48h1hRedução de 95%
Ocupação de Voos75%82%+7% de ocupação
Satisfação do ClienteNPS 45NPS 52+15% de retenção

8. Explicando a aplicação

8.1 Função load_data()

Python
def load_data():
    script_dir = Path(__file__).parent
    data_path = script_dir / "dataset" / "airlines_flights_data.csv"

    if not data_path.exists():
        raise FileNotFoundError(...)

    print(f"📂 Carregando dados de: {data_path}")
    df = pd.read_csv(data_path)
    print(f"✅ Dados carregados: {df.shape[0]} linhas e {df.shape[1]} colunas")
    return df
Por que usar Path(__file__).parent?
  • __file__ é uma variável especial que contém o caminho do script atual
  • .parent obtém o diretório pai onde o script está localizado
  • Isso garante que o código funcione em qualquer ambiente, independentemente de onde o script esteja
Vantagens:
  • ✅ Funciona no Windows, Linux e macOS
  • ✅ Não depende de caminhos absolutos ou relativos fixos
  • ✅ O dataset pode estar em qualquer lugar, desde que esteja em dataset/ junto ao script

8.2 Função preprocess_data()

Python
def preprocess_data(df):
    # Amostragem para reduzir tempo de processamento
    df = df.sample(n=15000, random_state=42)

    # Remover coluna 'index' se existir
    if 'index' in df.columns:
        df = df.drop(['index'], axis=1)

    # Converter colunas de data/hora
    df['departure_time'] = pd.to_datetime(df['departure_time'], errors='coerce')
    df['arrival_time'] = pd.to_datetime(df['arrival_time'], errors='coerce')

    return df
Amostragem (sample(n=15000, random_state=42)):
  • Reduz o dataset para 15.000 registros
  • Acelera o treinamento sem perder representatividade
  • random_state=42 garante reprodutibilidade (sempre a mesma amostra)
Conversão de Datas (pd.to_datetime()):
  • Converte strings para objetos datetime
  • errors='coerce' converte valores inválidos para NaT (Not a Time)
  • Isso permite extrair features como dia da semana, mês, hora, etc.

8.3 Função prepare_features()

Python
def prepare_features(df):
    target = 'price'

    # Definir features (X) e target (y)
    X = df.drop(columns=['flight', 'stops', 'departure_time', 'arrival_time', target])
    y = df[target]

    # Identificar colunas categóricas
    categorical_cols = X.select_dtypes(include='object').columns.tolist()

    return X, y, categorical_cols
Por que remover flight, stops, departure_time e arrival_time?
ColunaMotivo da Remoção
flightIdentificador único (não traz informação preditiva)
stopsRemovido, mas poderia ser útil (número de escalas)
departure_time / arrival_timeJá foram convertidos para datetime, mas não serão usados diretamente
Identificação de variáveis categóricas:
  • select_dtypes(include='object') seleciona colunas de texto
  • Exemplos: airline, source_city, destination_city, class

8.4 Criação do Pré-processador e Pipelines

Python
# Criar pré-processador
preprocessor = ColumnTransformer(
    transformers=[
        ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_cols)
    ],
    remainder='passthrough'
)

# Criar pipelines
def create_pipelines(preprocessor):
    pipelines = {
        'random_forest': Pipeline([
            ('preprocessor', preprocessor),
            ('model', RandomForestRegressor(random_state=42, n_jobs=-1))
        ]),
        'decision_tree': Pipeline([
            ('preprocessor', preprocessor),
            ('model', DecisionTreeRegressor(random_state=42))
        ])
    }
    return pipelines
ColumnTransformer:
  • Aplica OneHotEncoder às colunas categóricas
  • remainder='passthrough' mantém as colunas numéricas inalteradas
  • Transforma dados como:
airline: "Vistara"[1, 0, 0, 0, 0, 0]  (one-hot encoding)
Pipeline:
  • Encadeia pré-processamento + modelo em uma única unidade
  • Facilita o uso com cross_val_score e fit/predict
RandomForestRegressor:
  • n_jobs=-1: usa todos os núcleos do processador para treinamento paralelo
  • random_state=42: reprodutibilidade

8.5 Função train_and_evaluate()

Python
def train_and_evaluate(X, y, pipelines):
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )

    results = {}

    for name, pipeline in pipelines.items():
        pipeline.fit(X_train, y_train)
        y_pred = pipeline.predict(X_test)

        r2 = r2_score(y_test, y_pred)
        rmse = np.sqrt(mean_squared_error(y_test, y_pred))

        results[name] = {
            'pipeline': pipeline,
            'r2': r2,
            'rmse': rmse,
            'y_pred': y_pred,
            'y_test': y_test
        }

        print(f"   ✅ R²: {r2:.4f}")
        print(f"   ✅ RMSE: {rmse:.2f}")

    return results, X_test, y_test
Divisão Treino/Teste (80%/20%):
  • 80% dos dados para treinar o modelo
  • 20% para testar (dados que o modelo nunca viu)
Métricas:
MétricaFórmulaInterpretação
R² (Coeficiente de Determinação)1 - (SS_res / SS_tot)Proporção da variância explicada pelo modelo. Quanto mais próximo de 1, melhor.
RMSE (Root Mean Square Error)√(Σ(y_true - y_pred)² / n)Erro médio na mesma unidade do target (preço). Quanto menor, melhor.
Armazenamento dos Resultados:
  • Salva predições e valores reais para análise posterior
  • Permite gerar gráficos de dispersão e resíduos

8.6 Função cross_validate()

Python
def cross_validate(X, y, pipelines):
    for name, pipeline in pipelines.items():
        scores = cross_val_score(
            pipeline, X, y,
            cv=5,
            scoring='r2',
            n_jobs=-1
        )
        
        print(f"   📈 Scores R²: {scores}")
        print(f"   📊 Média R²: {np.mean(scores):.4f}")
        print(f"   📊 Desvio padrão: {np.std(scores):.4f}")
Validação Cruzada com 5 Folds:
Fold 1: [Treino] [Treino] [Treino] [Treino] [Teste]
Fold 2: [Treino] [Treino] [Treino] [Teste] [Treino]
Fold 3: [Treino] [Treino] [Teste] [Treino] [Treino]
Fold 4: [Treino] [Teste] [Treino] [Treino] [Treino]
Fold 5: [Teste] [Treino] [Treino] [Treino] [Treino]
Por que usar validação cruzada?
  • Avalia a estabilidade do modelo
  • Detecta overfitting (se os scores variarem muito entre os folds)
  • Fornece uma estimativa mais robusta da performance
Interpretação dos Resultados:
  • Média alta (~0.956) → modelo bom
  • Desvio padrão baixo (~0.003) → modelo estável

8.7 Função save_results()

Python
def save_results(results, X_test, y_test):
    results_dir = Path(__file__).parent / "results"
    results_dir.mkdir(exist_ok=True)

    # Salvar métricas
    metrics_df = pd.DataFrame([
        {
            'model': name,
            'r2_score': data['r2'],
            'rmse': data['rmse']
        }
        for name, data in results.items()
    ])
    metrics_df.to_csv(results_dir / 'model_metrics.csv', index=False)

    # Salvar predições
    for name, data in results.items():
        pred_df = pd.DataFrame({
            'actual_price': data['y_test'].values,
            'predicted_price': data['y_pred']
        })
        pred_df.to_csv(results_dir / f'predictions_{name}.csv', index=False)
Criação do Diretório:
  • mkdir(exist_ok=True) cria o diretório se ele não existir
  • Evita erro caso o diretório já tenha sido criado
Arquivos Gerados:
ArquivoConteúdo
model_metrics.csvMétricas R² e RMSE para cada modelo
predictions_random_forest.csvPreços reais vs previstos (Random Forest)
predictions_decision_tree.csvPreços reais vs previstos (Decision Tree)

9. Referências e Código

O projeto completo está disponível no GitHub:

  • Link: Flight Price Prediction
  • Dataset: dataset/airlines_flights_data.csv
  • Modelo: Random Forest Regressor com R² de 0.964
  • Dependências: Python 3.13+, scikit-learn, pandas, matplotlib, seaborn

Fique ligado

Seja um Expert em Growth

Receba insights práticos sobre marketing, dados, performance e tecnologia direto no seu email.