← Voltar pro projeto

O Brasil Compra de Dia Útil: Storytelling Exploratório da Olist

O capítulo 1 montou o modelo relacional e o dataframe mestre. Agora eu vou deixar o dado falar visualmente, seis ângulos diferentes do mesmo negócio, antes de treinar qualquer modelo. Notebook completo, executado, público no Colab.

Temporal: o Brasil compra de dia útil

orders_com_hora = orders.dropna(subset=['order_purchase_timestamp']).copy()
orders_com_hora['weekday'] = orders_com_hora['order_purchase_timestamp'].dt.weekday
orders_com_hora['hour'] = orders_com_hora['order_purchase_timestamp'].dt.hour

weekday_hour = (
    orders_com_hora
    .groupby(['weekday', 'hour'])['order_id']
    .nunique()
    .reset_index()
    .rename(columns={'order_id': 'orders'})
)

Carregando dados reais...

168 células (7 dias × 24 horas), soma batendo exatamente com o total de pedidos, sempre bom conferir isso antes de confiar no gráfico. O pico é terça-feira às 14h, com 1.124 pedidos numa hora só. Olhando o total por dia da semana, segunda a sexta somam entre 14 e 16 mil pedidos cada, enquanto sábado cai pra 10.887 e domingo pra 11.960, uma queda de quase 30%. Por hora, o horário de almoço e começo de tarde (11h, 13h-16h) concentra o grosso do movimento, e a madrugada (3h-5h) praticamente para, menos de 300 pedidos em cada uma dessas horas no dataset inteiro. Isso é o padrão clássico de "compra de trabalho remoto ou pausa do escritório", não de compra de lazer noturno.

Geográfico: o Norte espera mais

orders_com_cliente = orders.merge(customers, on='customer_id', how='left')

frete_por_pedido = order_items.groupby('order_id')['freight_value'].sum().reset_index()
orders_com_cliente = orders_com_cliente.merge(frete_por_pedido, on='order_id', how='left')

orders_com_cliente['delivery_days'] = (
    orders_com_cliente['order_delivered_customer_date'] - orders_com_cliente['order_purchase_timestamp']
).dt.days

por_estado = orders_com_cliente.groupby('customer_state').agg(
    orders=('order_id', 'nunique'),
    avg_freight=('freight_value', 'mean'),
    avg_delivery_days=('delivery_days', 'mean'),
).reset_index()

Carregando dados reais...

O mapa mostra o tempo médio real de entrega (compra até entrega) por estado do cliente. Roraima lidera com quase 29 dias de espera média (28,98), seguido por Amapá (26,73), Amazonas (25,99) e Alagoas (24,04). Não é coincidência esses estados aparecerem no topo: Roraima, Amapá e Amazonas são os três estados mais distantes do eixo industrial e logístico do Sudeste, onde a maior parte dos vendedores da Olist está concentrada. São Paulo, pra comparação, fica bem mais perto da ponta rápida dessa distribuição. Essa relação distância-tempo de entrega é candidata natural a feature forte quando eu for montar o cenário de previsão de atraso.

Categoria: beleza e saúde lidera o faturamento

top_categorias = (
    mestre
    .dropna(subset=['product_category_name_english'])
    .groupby('product_category_name_english')['price']
    .sum()
    .sort_values(ascending=False)
    .head(15)
    .reset_index()
)

Carregando dados reais...

health_beauty (beleza e saúde) lidera com 1.301.947,97 dólares em receita somada, seguida de perto por watches_gifts (relógios e presentes, 1.254.322,95) e bed_bath_table (cama, mesa e banho, 1.107.249,09). É receita, não é frete nem quantidade de item, então uma categoria pode liderar por vender caro (relógio) ou por vender muito (cama/mesa/banho é item de reposição frequente).

Pagamento: cartão de crédito domina

tipos_pagamento = (
    payments[payments['payment_type'] != 'not_defined']
    .groupby('payment_type')['order_id']
    .nunique()
    .reset_index()
    .rename(columns={'order_id': 'count'})
    .sort_values('count', ascending=False)
)

Carregando dados reais...

Cartão de crédito domina disparado: 76.505 pedidos, mais de 3 vezes o segundo colocado (boleto, 19.784). Voucher vem com 3.866 e cartão de débito com só 1.528. Descartei a categoria not_defined do gráfico, tinha só 3 linhas em mais de 100 mil pagamentos, ruído puro, não merece fatia de pizza.

Satisfação: a maioria ama, mas quem odeia é vocal

distribuicao_notas = (
    reviews
    .groupby('review_score')['review_id']
    .count()
    .reset_index()
    .rename(columns={'review_id': 'count'})
)

Carregando dados reais...

57.328 avaliações de nota 5, mais da metade do total, e nota 4 vem em segundo com 19.142. Mas repara no terceiro lugar: nota 1, com 11.424 avaliações, sozinha soma mais que nota 2 (3.151) e nota 3 (8.179) juntas. Isso é o padrão clássico de review de e-commerce: cliente satisfeito às vezes nem avalia, cliente neutro quase nunca avalia, e cliente muito insatisfeito sempre avalia pra desabafar. Uma distribuição assim bimodal (pico grande em 5, segundo pico menor em 1) já é um aviso pra quando eu for montar o cenário de previsão de nota: não é uma escala contínua bem comportada, é quase uma decisão "adorei ou detestei" com um meio-termo raro.

Junto com isso, uma amostra real (400 pedidos) cruzando atraso da entrega com a nota dada:

nota_atraso = (
    orders[['order_id', 'order_delivered_customer_date', 'order_estimated_delivery_date']]
    .merge(reviews[['order_id', 'review_score']], on='order_id', how='inner')
    .dropna(subset=['order_delivered_customer_date', 'order_estimated_delivery_date', 'review_score'])
)

nota_atraso['delay_days'] = (
    nota_atraso['order_delivered_customer_date'] - nota_atraso['order_estimated_delivery_date']
).dt.days

Carregando dados reais...

delay_days negativo é entrega adiantada (chegou antes do prometido), positivo é atraso de verdade. Repara que os pontos de nota 1 (a linha de baixo) aparecem espalhados por todo o eixo, inclusive nos negativos, mas com uma concentração visualmente maior no lado direito (atraso) do que os pontos de nota 5.

Correlação: o punchline antes do modelo

features_numericas = mestre[['price', 'freight_value', 'product_weight_g', 'payment_value', 'review_score']].copy()
features_numericas['delivery_days'] = (
    mestre['order_delivered_customer_date'] - mestre['order_purchase_timestamp']
).dt.days
features_numericas = features_numericas.dropna()

matriz_corr = features_numericas.corr()

Carregando dados reais...

Aqui está o número que eu prometi lá no fechamento do capítulo 1: review_score correlaciona -0,30 com delivery_days, calculado em cima de 114.838 linhas do dataframe mestre. É a correlação mais forte que qualquer feature numérica tem com a nota de avaliação (preço, frete e peso do produto ficam todos abaixo de 0,08 em módulo). Não é uma correlação gigante (longe de -1), mas é claramente a mais relevante do grupo, e bate exatamente com o que o mapa geográfico já insinuava: entrega demorada é o fator que mais aparenta puxar a satisfação do cliente pra baixo, mais do que quanto o produto custou ou pesou.

Fechando o capítulo

Seis ângulos, um padrão emergindo: tempo de entrega aparece três vezes (mapa, scatter, correlação) como o fio condutor mais forte pra explicar insatisfação. Cartão de crédito domina pagamento, beleza/saúde domina receita, terça-feira à tarde é o pico de compra. No próximo capítulo eu entro em engenharia e seleção de feature pros quatro cenários definidos no capítulo 1, e a discussão de vazamento de dado no cenário de atraso vai ficar bem mais concreta com esses números geográficos e de correlação já na mão.