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

- Published on

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ário | Benefício | Exemplo Prático |
|---|---|---|
| Otimização de Receita (Revenue Management) | Ajuste dinâmico de preços baseado em demanda prevista | Antecipar picos de demanda em feriados e aumentar preços estrategicamente |
| Planejamento de Frota e Rotas | Decisões baseadas em dados sobre quais rotas são mais lucrativas | Identificar que voos para destinos turísticos têm maior elasticidade de preço |
| Marketing Personalizado | Oferecer descontos para perfis de passageiros com menor disposição a pagar | Enviar ofertas específicas para passageiros que buscam rotas com baixa ocupação prevista |
| Gestão de Custos | Alinhar preços com custos operacionais em tempo real | Prever quedas de demanda e ajustar ofertas para manter ocupação mínima |
| Benchmarking Competitivo | Monitorar preços da concorrência e reagir rapidamente | Ajustar 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_timeearrival_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:

Principais Etapas:
- Amostragem: Redução para 15.000 registros para balancear tempo de processamento e representatividade
- Tratamento de Datas: Conversão de
departure_timeearrival_timepara datetime - Codificação de Variáveis Categóricas: Uso de OneHotEncoder para variáveis como
airline - 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:

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:

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:

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:
| Modelo | R² Score | RMSE |
|---|---|---|
| Random Forest | 0.964 | 4,344.82 |
| Decision Tree | 0.935 | 5,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)
| Voos | Preço Real | Preço Previsto | Erro |
|---|---|---|---|
| Voo A | R$ 10,058 | R$ 11,029 | +9.6% |
| Voo B | R$ 5,318 | R$ 5,086 | -4.4% |
| Voo C | R$ 50,271 | R$ 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:
- Oportunidade: Voos de baixo custo podem ter fatores não capturados (ex: promoções relâmpago)
- 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

6.2 Estratégia de Implantação
| Fase | Ação | Prazo |
|---|---|---|
| Pilot | Teste com dados históricos de 1 mês | Semana 1-2 |
| Shadow Mode | Execução paralela sem interferir em decisões | Semana 3-4 |
| Canary Deployment | 5% das requisições usando o novo modelo | Semana 5-6 |
| Full Production | 100% das requisições com monitoramento intensivo | Semana 7+ |
6.3 Métricas de Monitoramento Contínuo
- Performance: R² e RMSE recalculados semanalmente
- Data Drift: Monitorar mudanças nas distribuições de
durationedays_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
| Indicador | Antes | Depois | Impacto Estimado |
|---|---|---|---|
| Precisão de Precificação | Manual/Reativo | Preditiva | +15-20% em receita |
| Tempo de Resposta a Mercado | 48h | 1h | Redução de 95% |
| Ocupação de Voos | 75% | 82% | +7% de ocupação |
| Satisfação do Cliente | NPS 45 | NPS 52 | +15% de retenção |
8. Explicando a aplicação
8.1 Função load_data()
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.parentobté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()
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=42garante reprodutibilidade (sempre a mesma amostra)
Conversão de Datas (pd.to_datetime()):
- Converte strings para objetos datetime
errors='coerce'converte valores inválidos paraNaT(Not a Time)- Isso permite extrair features como dia da semana, mês, hora, etc.
8.3 Função prepare_features()
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?
| Coluna | Motivo da Remoção |
|---|---|
flight | Identificador único (não traz informação preditiva) |
stops | Removido, mas poderia ser útil (número de escalas) |
departure_time / arrival_time | Já 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
# 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_scoreefit/predict
RandomForestRegressor:
n_jobs=-1: usa todos os núcleos do processador para treinamento paralelorandom_state=42: reprodutibilidade
8.5 Função train_and_evaluate()
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étrica | Fórmula | Interpretaçã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()
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()
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:
| Arquivo | Conteúdo |
|---|---|
model_metrics.csv | Métricas R² e RMSE para cada modelo |
predictions_random_forest.csv | Preços reais vs previstos (Random Forest) |
predictions_decision_tree.csv | Preç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.