1 Introdução

O setor de e-commerce no Brasil representa um domínio dinâmico e em expansão, caracterizado por desafios logísticos únicos devido à vasta extensão territorial e desigualdades regionais em infraestrutura. Este projeto aborda o problema de compreender os padrões operacionais e comportamentais no e-commerce brasileiro, utilizando o Brazilian E-Commerce Public Dataset by Olist, disponível no Kaggle (https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce). Para plataformas de marketplace e vendedores em geral, os insights derivados dessa análise podem otimizar estratégias de precificação, logística e retenção de clientes, contribuindo para a eficiência econômica em um mercado projetado para crescer.

Essa análise beneficiará potenciais clientes, como gerentes de e-commerce, ao fornecer evidências quantitativas para decisões informadas, como alocação de recursos logísticos ou campanhas de marketing segmentadas.

1.1 Descrição do Dataset

O dataset Olist é composto por nove arquivos CSV, coletados de transações reais no Brasil entre setembro de 2016 e outubro de 2018, com propósito original de facilitar análises acadêmicas e profissionais sobre o e-commerce. Originalmente, inclui mais de 100 variáveis distribuídas em tabelas relacionais, cobrindo aspectos como clientes (e.g., CEP, estado), geolocalização (lat/lon), pedidos (status, timestamps), itens de pedido (preço, frete), pagamentos (tipo, valor), reviews (score, comentário), produtos (categoria, dimensões), vendedores (CEP, estado) e tradução de categorias. Peculiaridades incluem NAs em timestamps de entrega (indicando pedidos incompletos), outliers em preços/fretes (possíveis bugs ou promoções extremas), e nomes de cidades não padronizados (com acentos, abreviações). Valores ausentes em reviews são comuns, refletindo opcionalidade de comentários.

2 Pacotes Requeridos e Carregamento Inicial

  • readr: Para leitura eficiente de arquivos CSV, otimizando desempenho com grandes datasets.
  • dplyr: Para manipulação de dados via pipe (%>%), facilitando filtros, mutates e joins.
  • lubridate: Para operações em datas/timestamps, essencial para análises temporais.
  • stringr e stringi: Para processamento de strings, como normalização de nomes de cidades.
  • skimr: Para resumos iniciais de datasets, identificando tipos e NAs.
  • tidyr: ’Útil em agregações.
  • ggplot2: Para visualizações.
  • scales: Para formatação (moeda, datas) em plots.
  • leaflet: Para mapas interativos, integrando coordenadas geográficas.
  • geosphere: Para cálculos de distância.
library(readr)
library(dplyr)
library(lubridate)
library(stringr)
library(stringi)
library(skimr)
library(tidyr)
library(ggplot2)
library(scales)
library(leaflet)
library(geosphere)

customers <- read_csv("olist_customers_dataset.csv")
geolocation <- read_csv("olist_geolocation_dataset.csv")
order_items <- read_csv("olist_order_items_dataset.csv")
order_payments <- read_csv("olist_order_payments_dataset.csv")
order_reviews <- read_csv("olist_order_reviews_dataset.csv")
orders <- read_csv("olist_orders_dataset.csv")
products <- read_csv("olist_products_dataset.csv")
sellers <- read_csv("olist_sellers_dataset.csv")
category_translation <- read_csv("product_category_name_translation.csv")

brazil_states_regex <- "\\b(ac|ap|am|pa|ro|rr|to|al|ba|ce|ma|pb|pe|pi|rn|se|go|mt|ms|df|es|mg|rj|sp|pr|rs|sc)\\b"
regex_sudeste <- "\\b(sp|rj|mg|es)\\b"
regex_sul <- "\\b(pr|rs|sc)\\b"
regex_nordeste <- "\\b(ba|pe|ce|rn|pb|pi|al|se|ma)\\b"
regex_centro_oeste <- "\\b(go|mt|ms|df)\\b"
regex_norte <- "\\b(pa|am|ac|ro|rr|ap|to)\\b"

3 Preparação dos Dados

3.1 Análise Exploratória Inicial

Iniciei com skimr para avaliar estruturas, tipos de variáveis e NAs, identificando questões como ausências em timestamps de pedidos (e.g., entregas pendentes) e dimensões de produtos. Vejam a baixo o exemplo de orders.

skim(orders)
Data summary
Name orders
Number of rows 99441
Number of columns 8
_______________________
Column type frequency:
character 3
POSIXct 5
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
order_id 0 1 32 32 0 99441 0
customer_id 0 1 32 32 0 99441 0
order_status 0 1 7 11 0 8 0

Variable type: POSIXct

skim_variable n_missing complete_rate min max median n_unique
order_purchase_timestamp 0 1.00 2016-09-04 21:15:19 2018-10-17 17:30:18 2018-01-18 23:04:36 98875
order_approved_at 160 1.00 2016-09-15 12:16:38 2018-09-03 17:40:06 2018-01-19 11:36:13 90733
order_delivered_carrier_date 1783 0.98 2016-10-08 10:34:01 2018-09-11 19:48:28 2018-01-24 16:10:58 81018
order_delivered_customer_date 2965 0.97 2016-10-11 13:46:32 2018-10-17 13:22:46 2018-02-02 19:28:10 95664
order_estimated_delivery_date 0 1.00 2016-09-30 00:00:00 2018-11-12 00:00:00 2018-02-15 00:00:00 459

3.2 Normalização de Nomes de Cidades

Nomes de cidades possuiam diversas inconsistências (acentos, maiúsculas/minúsculas, abreviações de estados anexadas, etc…), potencialmente causando duplicatas em agregações geográficas. Foi desenvolvida uma função para padronização, removendo pontuações e invalidando entradas inválidas, garantindo consistência lógica para análises subsequentes.

normalize_city <- function(cities) {
  cities %>%
    str_to_lower() %>%
    str_remove(., paste0("\\s+", brazil_states_regex, "$")) %>%
    str_replace(., ".+@.+\\..+", NA_character_) %>%
    str_extract(., "^[^/\\\\,]+") %>%
    str_remove_all(., "\\(.*?\\)") %>%
    str_replace_all(., "d'|d´|d`", "d'") %>%
    str_remove_all(., "[\\.'ºª´\\^~]") %>%
    str_remove_all(., "^[\\.]+") %>%
    str_replace_all(., "-", " ") %>%
    str_squish() %>%
    str_replace(., paste0("^", brazil_states_regex, "$"), NA_character_)
}

customers <- customers %>% mutate(customer_city = normalize_city(customer_city))
sellers <- sellers %>% mutate(seller_city = normalize_city(seller_city))
geolocation <- geolocation %>% mutate(geolocation_city = normalize_city(geolocation_city))

3.3 Tratamento de Coordenadas Geográficas

O dataset geolocation continha múltiplas entradas por prefixo de CEP, o que é correto, porém interfere na visualização de mapas. Agreguei por média de lat/lon para obter pontos representativos, reduzindo ruído e facilitando visualizações espaciais sem perda significativa de precisão.

geolocation_per_zip_code <- geolocation %>%
  group_by(geolocation_zip_code_prefix) %>%
  summarise(
    geolocation_lat = mean(geolocation_lat, na.rm = TRUE),
    geolocation_lng = mean(geolocation_lng, na.rm = TRUE)
  ) %>%
  ungroup()

3.4 Tratamento de Valores Ausentes e Outliers

Em produtos, NAs em dimensões foram imputados com medianas para preservar distribuições centrais. Em order_items, filtrei outliers acima 99% em preço/frete para mitigar impactos de valores extremos em estatísticas descritivas.

products <- products %>%
  mutate(
    product_length_cm = ifelse(is.na(product_length_cm), median(product_length_cm, na.rm = TRUE), product_length_cm),
    product_height_cm = ifelse(is.na(product_height_cm), median(product_height_cm, na.rm = TRUE), product_height_cm),
    product_width_cm = ifelse(is.na(product_width_cm), median(product_width_cm, na.rm = TRUE), product_width_cm),
    product_weight_g = ifelse(is.na(product_weight_g), median(product_weight_g, na.rm = TRUE), product_weight_g)
  )

order_items <- order_items %>%
  filter(price < quantile(price, 0.99, na.rm = TRUE) & freight_value < quantile(freight_value, 0.99, na.rm = TRUE))

3.5 Tradução de Categorias de Produtos

Integrai a tradução para inglês via left_join, substituindo nomes originais para consistência analítica, mantendo foco em categorias padronizadas.

products <- products %>%
  left_join(category_translation, by = "product_category_name") %>%
  mutate(product_category_name = ifelse(is.na(product_category_name_english), product_category_name, product_category_name_english)) %>%
  select(-product_category_name_english)

3.6 Pré-Agregações

Cada pedido pode ter 1 ou mais itens e formas de pagamento, então agreguei pagamentos somando valores totais e selecionei tipo principal para evitar duplicatas. Para reviews, algo parecido acontece, pois um cliente pode enviar mais de uma review por pedido, então selecionei a mais recente e imputei sem_comentario em comentários e títulos vazios.

payments_agg_valor <- order_payments %>%
  group_by(order_id) %>%
  summarise(valor_total_pedido = sum(payment_value, na.rm = TRUE))

payments_agg_tipo <- order_payments %>%
  filter(payment_sequential == 1) %>%
  select(order_id, tipo_pagamento_principal = payment_type)

payments_final <- payments_agg_valor %>%
  left_join(payments_agg_tipo, by = "order_id")
reviews_final <- order_reviews %>%
  mutate(
    review_comment_message = replace_na(review_comment_message, "sem_comentario"),
    review_comment_title = replace_na(review_comment_title, "sem_comentario"),
    review_creation_date = as_datetime(review_creation_date)
  ) %>%
  group_by(order_id) %>%
  arrange(desc(review_creation_date)) %>%
  slice(1) %>%
  ungroup() %>%
  select(order_id, review_score, review_comment_message)

3.7 Construção das Tabelas Analíticas

Construí três tabelas via joins: tb_comercial (vendas/produtos), tb_logistica (com distância e estimativas de entrega), tb_clientes (agregação por cliente único). Durante o join foram selecionadas apenas as colunas necessárias para cada tabela analítica.

tb_comercial <- order_items %>%
  select(order_id, order_item_id, product_id, seller_id, price, freight_value) %>%
  left_join(orders %>% select(order_id, order_status, order_purchase_timestamp), by = "order_id") %>%
  left_join(products, by = "product_id") %>%
  left_join(sellers %>% select(seller_id, seller_city, seller_state), by = "seller_id") %>%
  left_join(payments_final, by = "order_id")
geo_cust <- geolocation_per_zip_code %>% 
  rename(lat_cli = geolocation_lat, lng_cli = geolocation_lng)

geo_sell <- geolocation_per_zip_code %>% 
  rename(lat_venda = geolocation_lat, lng_venda = geolocation_lng)

tb_logistica <- order_items %>%
  select(order_id, order_item_id, seller_id, freight_value) %>%
  left_join(orders, by = "order_id") %>%
  left_join(sellers %>% select(seller_id, seller_zip_code_prefix, seller_state), by = "seller_id") %>%
  left_join(customers %>% select(customer_id, customer_zip_code_prefix, customer_state), by = "customer_id") %>%
  left_join(geo_sell, by = c("seller_zip_code_prefix" = "geolocation_zip_code_prefix")) %>%
  left_join(geo_cust, by = c("customer_zip_code_prefix" = "geolocation_zip_code_prefix")) %>%
  mutate(distancia_km = distHaversine(matrix(c(lng_venda, lat_venda), ncol = 2), 
                                      matrix(c(lng_cli, lat_cli), ncol = 2)) / 1000)
tb_clientes <- orders %>%
  left_join(customers, by = "customer_id") %>%
  left_join(payments_final, by = "order_id") %>%
  left_join(reviews_final, by = "order_id") %>%
  group_by(customer_unique_id) %>%
  summarise(
    cidade = first(customer_city),
    estado = first(customer_state),
    total_pedidos = n(),
    total_gasto = sum(valor_total_pedido, na.rm = TRUE),
    data_primeira_compra = min(order_purchase_timestamp),
    data_ultima_compra = max(order_purchase_timestamp),
    nota_media = mean(review_score, na.rm = TRUE)
  ) %>%
  ungroup()

3.8 Engenharia de Variáveis

Criei variáveis derivadas: volume/densidade em produtos para correlações logísticas; métricas temporais e classificações RFM(Recência, Frequência e Valor Monetário) em clientes para calcular a recência de compra. Ao final, removi colunas que não seriam substituidas pelas novas variaveis.

tb_comercial <- tb_comercial %>%
  mutate(
    volume_produto_cm3 = product_length_cm * product_height_cm * product_width_cm,
    densidade_produto = product_weight_g / volume_produto_cm3
  )
tb_logistica <- tb_logistica %>%
  mutate(
    tempo_processamento_horas = as.numeric(difftime(order_approved_at, order_purchase_timestamp, units = "hours")),
    tempo_entrega_total_dias = as.numeric(difftime(order_delivered_customer_date, order_purchase_timestamp, units = "days")),
    atraso_entrega_dias = as.numeric(difftime(order_delivered_customer_date, order_estimated_delivery_date, units = "days")),
    status_sla = case_when(
      atraso_entrega_dias > 0 ~ "Atrasado",
      TRUE ~ "No Prazo/Adiantado"
    )
  )
data_ref <- max(orders$order_purchase_timestamp, na.rm = TRUE) + days(1)

tb_clientes <- tb_clientes %>%
  mutate(
    tipo_cliente = ifelse(total_pedidos > 1, "Recorrente", "Novo"),
    recencia_dias = as.numeric(difftime(data_ref, data_ultima_compra, units = "days")),
    ticket_medio = total_gasto / total_pedidos
  )
get_region <- function(state) {
  case_when(
    str_detect(str_to_lower(state), regex_norte) ~ "Norte",
    str_detect(str_to_lower(state), regex_nordeste) ~ "Nordeste",
    str_detect(str_to_lower(state), regex_centro_oeste) ~ "Centro-Oeste",
    str_detect(str_to_lower(state), regex_sudeste) ~ "Sudeste",
    str_detect(str_to_lower(state), regex_sul) ~ "Sul",
    TRUE ~ "Outros"
  )
}

tb_clientes <- tb_clientes %>% mutate(regiao = get_region(estado))
tb_logistica <- tb_logistica %>% mutate(regiao_cliente = get_region(customer_state), regiao_vendedor = get_region(seller_state))
tb_comercial <- tb_comercial %>% mutate(regiao_vendedor = get_region(seller_state))
tb_comercial <- tb_comercial %>%
  select(-product_name_lenght, -product_description_lenght, -product_photos_qty,
         -product_length_cm, -product_height_cm, -product_width_cm)

tb_logistica <- tb_logistica %>%
  select(-order_approved_at, -order_delivered_carrier_date)

3.9 Dataset Final e Resumo de Variáveis

Após limpeza, as tabelas analíticas estão prontas. Aqui, um resumo consolidado via tabela para variáveis numéricas em tb_logistica (exemplo), destacando tipos, estatísticas e NAs remanescentes.

Para variáveis categóricas, computamos distribuições separadamente. Por exemplo, para status_sla:

Variáveis categóricas como status_sla exibem distribuições balanceadas (92.3% no prazo), enquanto distâncias variam de 0 a 8687.6328988 km, com média 591 km.

4 Análise Exploratória dos Dados

4.1 Evolução Temporal das Vendas

Agreguei por mês para revelar tendências. O volume cresce exponencialmente, com pico em Nov/2017 (Black Friday).

vendas_temporal <- tb_comercial %>%
  mutate(mes_ano = floor_date(order_purchase_timestamp, "month")) %>%
  group_by(mes_ano) %>%
  summarise(
    total_pedidos = n_distinct(order_id),
    total_receita = sum(price, na.rm = TRUE)
  ) %>%
  filter(mes_ano >= "2017-01-01" & mes_ano <= "2018-08-01")

# Cálculo do percentual do pico
vendas_2017 <- vendas_temporal %>% filter(lubridate::year(mes_ano) == 2017)
pico_percent <- round(max(vendas_2017$total_pedidos) / sum(vendas_2017$total_pedidos) * 100, 1)

ggplot(vendas_temporal, aes(x = mes_ano, y = total_pedidos)) +
  geom_line(color = "#2c3e50", size = 1) +
  geom_point(color = "#c0392b", size = 2) +
  geom_smooth(method = "loess", se = FALSE, color = "gray", linetype = "dashed") +
  theme_minimal() +
  labs(title = "Evolução Mensal do Volume de Pedidos",
       x = "Mês", y = "Número de Pedidos") +
  scale_x_datetime(date_breaks = "2 months", date_labels = "%b/%y")

4.2 Análise Logística

Mapa interativo via leaflet revela concentração no Sudeste, com amostra de 5000 para performance.

leaflet(tb_logistica %>% sample_n(5000)) %>%
  addTiles() %>%
  addCircleMarkers(lng = ~lng_cli, lat = ~lat_cli, radius = 1, color = "blue", opacity = 0.5) %>%
  addCircleMarkers(lng = ~lng_venda, lat = ~lat_venda, radius = 1, color = "red", opacity = 0.5)

Resumo regional via group_by e pivot_longer destaca desigualdades: Norte com frete médio 1.9x Sudeste e taxa de atraso 2% maior.

resumo_regional_long <- resumo_regional %>%
  pivot_longer(cols = -regiao_cliente, names_to = "metrica", values_to = "valor")

ggplot(resumo_regional_long, aes(x = regiao_cliente, y = valor, fill = regiao_cliente)) +
  geom_col() +
  facet_wrap(~metrica, scales = "free_y") +
  theme_minimal() +
  labs(title = "Métricas Logísticas por Região", x = "Região", y = "Valor")

4.3 Análise de Produtos e Categorias

Top 10 categorias por receita.

top_categorias <- tb_comercial %>%
  group_by(product_category_name) %>%
  summarise(receita = sum(price, na.rm = TRUE)) %>%
  top_n(10, receita)

ggplot(top_categorias, aes(x = reorder(product_category_name, receita), y = receita)) +
  geom_col(fill = "#2c3e50") +
  coord_flip() +
  theme_minimal() +
  labs(title = "Top Categorias por Receita", x = "Categoria", y = "Receita (R$)") +
  scale_y_continuous(labels = scales::dollar_format(prefix = "R$ ", big.mark = ".", decimal.mark = ","))

4.4 Análise de Clientes (RFM)

Proporção de recorrentes 3.1%, via pie chart.

ggplot(fidelidade, aes(x = "", y = prop, fill = tipo_cliente)) +
  geom_bar(stat = "identity") +
  coord_polar("y") +
  theme_void() +
  labs(title = "Proporção de Clientes por Tipo")

4.5 Ticket médio por região

ticket_reg <- tb_clientes %>%
  group_by(regiao) %>%
  summarise(ticket_medio = mean(ticket_medio, na.rm = TRUE)) %>%
  filter(regiao != "Outros")

ggplot(ticket_reg, aes(x = reorder(regiao, ticket_medio), y = ticket_medio, fill = regiao)) +
  geom_col() +
  theme_minimal() +
  labs(title = "Ticket Médio por Região", x = "Região", y = "Ticket (R$)")

5 Conclusões (Em andamento)

Insights até o momento: Crescimento sazonal com pico Black Friday; desigualdades logísticas no Norte/Nordeste (frete/tempo elevados); categorias como health_beauty dominando receita; baixa recorrência (3.1%); correlações frete-distância.