Este relatório investiga como fatores logísticos, como tempo de entrega, atraso e valor do frete, podem influenciar a satisfação dos clientes no e-commerce brasileiro. A análise utiliza dados públicos da Olist e transforma várias tabelas separadas em uma base analítica única.
O crescimento do comércio eletrônico no Brasil tornou a experiência de compra cada vez mais dependente de fatores logísticos. Em uma compra online, o consumidor não avalia apenas o produto adquirido, mas também o prazo de entrega, o valor do frete, a confiabilidade da previsão de chegada e a qualidade geral do serviço.
Pergunta central do projeto:
Como o tempo de
entrega, os atrasos logísticos e o custo do frete influenciam a
avaliação dos clientes no e-commerce brasileiro?
Para responder essa questão, será utilizado o conjunto de dados Brazilian E-Commerce Public Dataset by Olist, disponível gratuitamente no Kaggle. A base contém dados reais e anonimizados de pedidos realizados em uma plataforma brasileira de e-commerce, distribuídos em várias tabelas relacionais.
A abordagem adotada consiste em importar os dados, realizar limpeza e organização, mesclar tabelas diferentes e criar variáveis derivadas relacionadas à logística e satisfação, como tempo de entrega, dias de atraso, status de atraso e percentual do frete sobre o valor do produto. Em seguida, serão feitas análises exploratórias com gráficos e tabelas para identificar padrões relevantes.
Por que essa análise importa?
Empresas de
e-commerce e marketplaces podem usar esse tipo de análise para
identificar gargalos logísticos, entender regiões com maior risco de
atraso e melhorar a experiência do consumidor.
library(tidyverse)
library(readr)
library(dplyr)
library(lubridate)
library(ggplot2)
library(plotly)
library(DT)
library(knitr)
library(scales)
library(forcats)
library(glue)pacotes <- tibble(
pacote = c("tidyverse", "readr", "dplyr", "lubridate", "ggplot2", "plotly", "DT", "knitr", "scales", "forcats"),
finalidade = c(
"Conjunto de ferramentas para manipulação, limpeza e visualização de dados.",
"Importação de arquivos CSV.",
"Filtros, agrupamentos, criação de variáveis e junções entre tabelas.",
"Conversão e manipulação de datas.",
"Criação de gráficos estáticos.",
"Criação de gráficos interativos.",
"Criação de tabelas interativas.",
"Organização de tabelas no relatório.",
"Formatação de valores monetários, percentuais e escalas.",
"Organização de variáveis categóricas."
)
)
knitr::kable(pacotes, caption = "Pacotes utilizados e suas finalidades")| pacote | finalidade |
|---|---|
| tidyverse | Conjunto de ferramentas para manipulação, limpeza e visualização de dados. |
| readr | Importação de arquivos CSV. |
| dplyr | Filtros, agrupamentos, criação de variáveis e junções entre tabelas. |
| lubridate | Conversão e manipulação de datas. |
| ggplot2 | Criação de gráficos estáticos. |
| plotly | Criação de gráficos interativos. |
| DT | Criação de tabelas interativas. |
| knitr | Organização de tabelas no relatório. |
| scales | Formatação de valores monetários, percentuais e escalas. |
| forcats | Organização de variáveis categóricas. |
Os dados utilizados neste projeto foram obtidos no Kaggle, no conjunto Brazilian E-Commerce Public Dataset by Olist.
Link da fonte original: https://www.kaggle.com/datasets/olistbr/brazilian-ecommerce
O conjunto possui dados de pedidos de e-commerce no Brasil, incluindo informações sobre clientes, pedidos, itens dos pedidos, pagamentos, avaliações, produtos, vendedores e localização geográfica. A base é relacional, ou seja, está dividida em diferentes arquivos que precisam ser unidos para permitir análises mais completas.
Observação importante:
Para executar este
relatório, baixe o dataset no Kaggle, extraia os arquivos e coloque
todos os arquivos CSV dentro de uma pasta chamada data, no
mesmo diretório deste arquivo RMarkdown.
caminho_dados <- "data"
orders <- read_csv(file.path(caminho_dados, "olist_orders_dataset.csv"))
customers <- read_csv(file.path(caminho_dados, "olist_customers_dataset.csv"))
items <- read_csv(file.path(caminho_dados, "olist_order_items_dataset.csv"))
payments <- read_csv(file.path(caminho_dados, "olist_order_payments_dataset.csv"))
reviews <- read_csv(file.path(caminho_dados, "olist_order_reviews_dataset.csv"))
products <- read_csv(file.path(caminho_dados, "olist_products_dataset.csv"))
category_translation <- read_csv(file.path(caminho_dados, "product_category_name_translation.csv"))dimensoes <- tibble(
tabela = c("Pedidos", "Clientes", "Itens", "Pagamentos", "Avaliações", "Produtos", "Tradução de categorias"),
objeto = c("orders", "customers", "items", "payments", "reviews", "products", "category_translation"),
linhas = c(nrow(orders), nrow(customers), nrow(items), nrow(payments), nrow(reviews), nrow(products), nrow(category_translation)),
colunas = c(ncol(orders), ncol(customers), ncol(items), ncol(payments), ncol(reviews), ncol(products), ncol(category_translation))
)
datatable(
dimensoes,
caption = "Dimensões das tabelas originais utilizadas no projeto",
options = list(pageLength = 10, dom = "tip")
)As datas originalmente são importadas como texto. Por isso, é necessário convertê-las para o formato de data e hora. Também serão criadas variáveis logísticas importantes para a análise.
orders_limpo <- orders %>%
mutate(
order_purchase_timestamp = ymd_hms(order_purchase_timestamp),
order_approved_at = ymd_hms(order_approved_at),
order_delivered_carrier_date = ymd_hms(order_delivered_carrier_date),
order_delivered_customer_date = ymd_hms(order_delivered_customer_date),
order_estimated_delivery_date = ymd_hms(order_estimated_delivery_date)
) %>%
filter(order_status == "delivered") %>%
filter(!is.na(order_delivered_customer_date)) %>%
mutate(
tempo_entrega_dias = as.numeric(difftime(order_delivered_customer_date, order_purchase_timestamp, units = "days")),
tempo_aprovacao_horas = as.numeric(difftime(order_approved_at, order_purchase_timestamp, units = "hours")),
dias_atraso = as.numeric(difftime(order_delivered_customer_date, order_estimated_delivery_date, units = "days")),
pedido_atrasado = if_else(dias_atraso > 0, "Atrasado", "No prazo"),
ano_mes_compra = floor_date(order_purchase_timestamp, "month")
)Decisão de limpeza:
A análise mantém apenas
pedidos entregues, pois o objetivo é comparar a data estimada com a data
real de entrega. Pedidos cancelados ou sem entrega registrada não
permitem calcular corretamente o tempo de entrega.
Como um mesmo pedido pode possuir mais de um item, os dados de itens
serão agregados por order_id.
items_agregado <- items %>%
group_by(order_id) %>%
summarise(
quantidade_itens = n(),
preco_produtos = sum(price, na.rm = TRUE),
valor_frete = sum(freight_value, na.rm = TRUE),
product_id = first(product_id),
seller_id = first(seller_id),
.groups = "drop"
) %>%
mutate(
valor_total_pedido = preco_produtos + valor_frete,
percentual_frete = if_else(preco_produtos > 0, valor_frete / preco_produtos, NA_real_)
)Um pedido pode possuir mais de uma avaliação. Para evitar duplicidade, as avaliações serão resumidas por pedido.
reviews_agregado <- reviews %>%
group_by(order_id) %>%
summarise(
nota_avaliacao = mean(review_score, na.rm = TRUE),
qtd_avaliacoes = n(),
.groups = "drop"
) %>%
mutate(
grupo_avaliacao = case_when(
nota_avaliacao <= 2 ~ "Baixa",
nota_avaliacao == 3 ~ "Média",
nota_avaliacao >= 4 ~ "Alta",
TRUE ~ NA_character_
)
)Como um pedido pode ter mais de um registro de pagamento, os pagamentos também serão agregados por pedido.
products_limpo <- products %>%
left_join(category_translation, by = "product_category_name") %>%
mutate(
categoria_produto = coalesce(product_category_name_english, product_category_name),
categoria_produto = str_replace_all(categoria_produto, "_", " "),
categoria_produto = str_to_title(categoria_produto)
) %>%
select(product_id, categoria_produto)dados_final <- orders_limpo %>%
left_join(customers, by = "customer_id") %>%
left_join(items_agregado, by = "order_id") %>%
left_join(reviews_agregado, by = "order_id") %>%
left_join(payments_agregado, by = "order_id") %>%
left_join(products_limpo, by = "product_id") %>%
mutate(
customer_state = as.factor(customer_state),
pedido_atrasado = as.factor(pedido_atrasado),
grupo_avaliacao = as.factor(grupo_avaliacao),
tipo_pagamento_principal = as.factor(tipo_pagamento_principal),
categoria_produto = fct_lump_n(as.factor(categoria_produto), n = 15)
)total_pedidos <- nrow(dados_final)
total_estados <- n_distinct(dados_final$customer_state)
nota_media_geral <- mean(dados_final$nota_avaliacao, na.rm = TRUE)
tempo_medio_geral <- mean(dados_final$tempo_entrega_dias, na.rm = TRUE)
cat(glue::glue('
<div class="metric-grid">
<div class="metric-card">
<div class="metric-value">{format(total_pedidos, big.mark = ".", decimal.mark = ",")}</div>
<div class="metric-label">Pedidos analisados</div>
</div>
<div class="metric-card">
<div class="metric-value">{total_estados}</div>
<div class="metric-label">Estados</div>
</div>
<div class="metric-card">
<div class="metric-value">{round(nota_media_geral, 2)}</div>
<div class="metric-label">Nota média geral</div>
</div>
<div class="metric-card">
<div class="metric-value">{round(tempo_medio_geral, 1)}</div>
<div class="metric-label">Dias médios de entrega</div>
</div>
</div>
'))<div class="metric-value">96.470</div>
<div class="metric-label">Pedidos analisados</div>
<div class="metric-value">27</div>
<div class="metric-label">Estados</div>
<div class="metric-value">4.16</div>
<div class="metric-label">Nota média geral</div>
<div class="metric-value">12.6</div>
<div class="metric-label">Dias médios de entrega</div>
dados_final %>%
select(
order_id,
customer_state,
order_purchase_timestamp,
tempo_entrega_dias,
dias_atraso,
pedido_atrasado,
preco_produtos,
valor_frete,
percentual_frete,
nota_avaliacao,
tipo_pagamento_principal,
categoria_produto
) %>%
head(30) %>%
datatable(
caption = "Amostra do dataset final limpo",
options = list(pageLength = 10, scrollX = TRUE)
)resumo_variaveis <- tibble(
variavel = c(
"tempo_entrega_dias",
"dias_atraso",
"preco_produtos",
"valor_frete",
"percentual_frete",
"nota_avaliacao",
"customer_state",
"pedido_atrasado",
"tipo_pagamento_principal",
"categoria_produto"
),
descricao = c(
"Quantidade de dias entre a compra e a entrega ao cliente.",
"Diferença entre a data real de entrega e a data estimada.",
"Soma do preço dos produtos do pedido.",
"Soma do valor de frete do pedido.",
"Proporção do frete em relação ao preço dos produtos.",
"Nota média de avaliação do pedido.",
"Estado do cliente.",
"Indica se o pedido foi entregue no prazo ou com atraso.",
"Tipo de pagamento com maior valor no pedido.",
"Categoria principal do produto."
),
tipo = c(
"Numérica",
"Numérica",
"Numérica",
"Numérica",
"Numérica",
"Numérica",
"Categórica",
"Categórica",
"Categórica",
"Categórica"
)
)
knitr::kable(resumo_variaveis, caption = "Resumo das variáveis de interesse")| variavel | descricao | tipo |
|---|---|---|
| tempo_entrega_dias | Quantidade de dias entre a compra e a entrega ao cliente. | Numérica |
| dias_atraso | Diferença entre a data real de entrega e a data estimada. | Numérica |
| preco_produtos | Soma do preço dos produtos do pedido. | Numérica |
| valor_frete | Soma do valor de frete do pedido. | Numérica |
| percentual_frete | Proporção do frete em relação ao preço dos produtos. | Numérica |
| nota_avaliacao | Nota média de avaliação do pedido. | Numérica |
| customer_state | Estado do cliente. | Categórica |
| pedido_atrasado | Indica se o pedido foi entregue no prazo ou com atraso. | Categórica |
| tipo_pagamento_principal | Tipo de pagamento com maior valor no pedido. | Categórica |
| categoria_produto | Categoria principal do produto. | Categórica |
Estratégia da análise:
A narrativa parte do
volume de pedidos, passa pela distribuição geográfica, aprofunda a
logística e finaliza conectando atraso, frete e avaliação dos
clientes.
pedidos_mes <- dados_final %>%
count(ano_mes_compra)
grafico_pedidos_tempo <- ggplot(pedidos_mes, aes(x = ano_mes_compra, y = n)) +
geom_line(linewidth = 1.1, color = "#16324f") +
geom_point(size = 2.2, color = "#0f766e") +
labs(
title = "Evolução mensal dos pedidos",
subtitle = "Quantidade de pedidos entregues ao longo do tempo",
x = "Mês da compra",
y = "Quantidade de pedidos"
) +
scale_y_continuous(labels = comma) +
theme_minimal(base_size = 14)
ggplotly(grafico_pedidos_tempo)A evolução mensal dos pedidos permite observar o comportamento da demanda ao longo do tempo. Esse tipo de análise é importante para identificar sazonalidade, períodos de maior volume e possíveis momentos de pressão sobre a operação logística.
pedidos_estado <- dados_final %>%
count(customer_state, sort = TRUE) %>%
mutate(customer_state = fct_reorder(customer_state, n))
grafico_pedidos_estado <- ggplot(pedidos_estado, aes(x = customer_state, y = n)) +
geom_col(fill = "#0f766e") +
coord_flip() +
labs(
title = "Quantidade de pedidos por estado",
subtitle = "Estados com maior volume concentram maior impacto operacional",
x = "Estado do cliente",
y = "Quantidade de pedidos"
) +
scale_y_continuous(labels = comma) +
theme_minimal(base_size = 14)
ggplotly(grafico_pedidos_estado)Essa análise mostra a concentração geográfica dos pedidos. Estados com maior número de pedidos podem exigir maior atenção logística, pois concentram parte significativa da operação.
entrega_estado <- dados_final %>%
group_by(customer_state) %>%
summarise(
pedidos = n(),
tempo_medio_entrega = mean(tempo_entrega_dias, na.rm = TRUE),
atraso_medio = mean(dias_atraso, na.rm = TRUE),
nota_media = mean(nota_avaliacao, na.rm = TRUE),
.groups = "drop"
) %>%
filter(pedidos >= 100) %>%
arrange(desc(tempo_medio_entrega))
grafico_entrega_estado <- entrega_estado %>%
mutate(customer_state = fct_reorder(customer_state, tempo_medio_entrega)) %>%
ggplot(aes(x = customer_state, y = tempo_medio_entrega)) +
geom_col(fill = "#2563eb") +
coord_flip() +
labs(
title = "Tempo médio de entrega por estado",
subtitle = "Considerando apenas estados com pelo menos 100 pedidos",
x = "Estado",
y = "Tempo médio de entrega em dias"
) +
theme_minimal(base_size = 14)
ggplotly(grafico_entrega_estado)O tempo médio de entrega por estado ajuda a identificar desigualdades logísticas regionais. Estados mais distantes dos principais centros de distribuição podem apresentar prazos maiores.
avaliacao_atraso <- dados_final %>%
group_by(pedido_atrasado) %>%
summarise(
pedidos = n(),
nota_media = mean(nota_avaliacao, na.rm = TRUE),
tempo_medio_entrega = mean(tempo_entrega_dias, na.rm = TRUE),
atraso_medio = mean(dias_atraso, na.rm = TRUE),
.groups = "drop"
)
knitr::kable(avaliacao_atraso, digits = 2, caption = "Comparação entre pedidos atrasados e entregues no prazo")| pedido_atrasado | pedidos | nota_media | tempo_medio_entrega | atraso_medio |
|---|---|---|---|---|
| NA | 96470 | 4.16 | 12.56 | NaN |
grafico_atraso_avaliacao <- ggplot(avaliacao_atraso, aes(x = pedido_atrasado, y = nota_media, fill = pedido_atrasado)) +
geom_col(show.legend = FALSE) +
labs(
title = "Nota média de avaliação por status de entrega",
subtitle = "Comparação entre pedidos entregues no prazo e pedidos atrasados",
x = "Status de entrega",
y = "Nota média"
) +
scale_fill_manual(values = c("Atrasado" = "#ef4444", "No prazo" = "#16a34a")) +
coord_cartesian(ylim = c(0, 5)) +
theme_minimal(base_size = 14)
ggplotly(grafico_atraso_avaliacao)Insight esperado:
Essa é uma das análises
centrais do projeto. Se a nota média dos pedidos atrasados for menor,
isso sugere que a experiência logística está associada à satisfação do
cliente.
set.seed(123)
amostra_dispersao <- dados_final %>%
filter(!is.na(dias_atraso), !is.na(nota_avaliacao)) %>%
sample_n(size = min(8000, n()))
grafico_dispersao <- ggplot(amostra_dispersao, aes(x = dias_atraso, y = nota_avaliacao)) +
geom_point(alpha = 0.22, color = "#16324f") +
geom_smooth(method = "lm", se = FALSE, color = "#ef4444") +
labs(
title = "Relação entre dias de atraso e nota de avaliação",
subtitle = "A linha indica a tendência geral entre atraso e satisfação",
x = "Dias de atraso",
y = "Nota de avaliação"
) +
theme_minimal(base_size = 14)
ggplotly(grafico_dispersao)O gráfico de dispersão permite observar a tendência geral entre atraso e avaliação. Caso a linha de tendência seja negativa, isso indica que pedidos mais atrasados tendem a receber notas menores.
dados_frete_grupo <- dados_final %>%
filter(!is.na(percentual_frete), percentual_frete >= 0) %>%
mutate(
faixa_frete = case_when(
percentual_frete <= 0.10 ~ "Até 10%",
percentual_frete <= 0.25 ~ "10% a 25%",
percentual_frete <= 0.50 ~ "25% a 50%",
percentual_frete <= 1.00 ~ "50% a 100%",
percentual_frete > 1.00 ~ "Acima de 100%",
TRUE ~ NA_character_
)
) %>%
group_by(faixa_frete) %>%
summarise(
pedidos = n(),
nota_media = mean(nota_avaliacao, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(faixa_frete = factor(faixa_frete, levels = c("Até 10%", "10% a 25%", "25% a 50%", "50% a 100%", "Acima de 100%")))
grafico_frete <- ggplot(dados_frete_grupo, aes(x = faixa_frete, y = nota_media)) +
geom_col(fill = "#f97316") +
labs(
title = "Nota média por faixa de percentual do frete",
subtitle = "Frete calculado em relação ao preço dos produtos",
x = "Frete em relação ao preço dos produtos",
y = "Nota média"
) +
coord_cartesian(ylim = c(0, 5)) +
theme_minimal(base_size = 14)
ggplotly(grafico_frete)Essa análise investiga se o peso do frete em relação ao preço do produto está associado à avaliação do cliente. Fretes proporcionalmente altos podem aumentar a expectativa do consumidor e tornar a experiência mais sensível a problemas de entrega.
categorias_atraso <- dados_final %>%
filter(!is.na(categoria_produto)) %>%
group_by(categoria_produto) %>%
summarise(
pedidos = n(),
atraso_medio = mean(dias_atraso, na.rm = TRUE),
nota_media = mean(nota_avaliacao, na.rm = TRUE),
.groups = "drop"
) %>%
filter(pedidos >= 100) %>%
arrange(desc(atraso_medio)) %>%
slice_head(n = 10)
grafico_categorias_atraso <- categorias_atraso %>%
mutate(categoria_produto = fct_reorder(categoria_produto, atraso_medio)) %>%
ggplot(aes(x = categoria_produto, y = atraso_medio)) +
geom_col(fill = "#7c3aed") +
coord_flip() +
labs(
title = "Categorias com maior atraso médio",
subtitle = "Considerando categorias com pelo menos 100 pedidos",
x = "Categoria",
y = "Atraso médio em dias"
) +
theme_minimal(base_size = 14)
ggplotly(grafico_categorias_atraso)A análise por categoria permite identificar grupos de produtos que podem apresentar maior complexidade logística. Produtos maiores, mais frágeis ou dependentes de vendedores específicos podem ter maior risco de atraso.
categorias_avaliacao <- dados_final %>%
filter(!is.na(categoria_produto)) %>%
group_by(categoria_produto) %>%
summarise(
pedidos = n(),
nota_media = mean(nota_avaliacao, na.rm = TRUE),
tempo_medio_entrega = mean(tempo_entrega_dias, na.rm = TRUE),
.groups = "drop"
) %>%
filter(pedidos >= 100) %>%
arrange(nota_media) %>%
slice_head(n = 10)
datatable(
categorias_avaliacao,
caption = "Categorias com pior avaliação média",
options = list(pageLength = 10)
)Essa tabela ajuda a identificar categorias que merecem atenção estratégica. Uma categoria com baixa avaliação pode indicar problemas de entrega, expectativa do consumidor, qualidade do produto ou atendimento.
pagamentos_resumo <- dados_final %>%
count(tipo_pagamento_principal, sort = TRUE) %>%
mutate(percentual = n / sum(n))
grafico_pagamentos <- ggplot(pagamentos_resumo, aes(x = fct_reorder(tipo_pagamento_principal, n), y = n)) +
geom_col(fill = "#0f766e") +
coord_flip() +
labs(
title = "Formas de pagamento mais utilizadas",
subtitle = "Perfil geral de pagamento dos pedidos analisados",
x = "Forma de pagamento",
y = "Quantidade de pedidos"
) +
scale_y_continuous(labels = comma) +
theme_minimal(base_size = 14)
ggplotly(grafico_pagamentos)Embora o foco principal do projeto seja logística e satisfação, a forma de pagamento ajuda a caracterizar o comportamento dos consumidores e a composição das compras realizadas.
A análise proposta buscou compreender como fatores logísticos influenciam a satisfação dos clientes no e-commerce brasileiro. Para isso, foram utilizados dados públicos da Olist, com informações sobre pedidos, clientes, itens, pagamentos, avaliações e produtos.
A preparação dos dados exigiu a junção de várias tabelas, conversão de datas, tratamento de registros ausentes, agregação de pedidos com múltiplos itens e criação de variáveis derivadas. Entre as variáveis criadas estão o tempo de entrega, os dias de atraso, o status de atraso e o percentual do frete sobre o preço dos produtos.
As análises exploratórias permitem investigar se pedidos atrasados recebem avaliações menores, quais estados apresentam maiores tempos de entrega, quais categorias possuem maior atraso médio e como o valor do frete se relaciona com a nota dos consumidores.
Mensagem principal:
A logística é uma parte
central da experiência do cliente no e-commerce. Quando o prazo
prometido não é cumprido ou quando o frete pesa muito no valor final, a
percepção do consumidor pode ser afetada.
Os resultados podem auxiliar empresas de e-commerce e marketplaces na identificação de gargalos logísticos e oportunidades de melhoria na experiência do cliente. Caso seja observado que atrasos estão fortemente relacionados a notas menores, a empresa pode priorizar melhorias no cumprimento dos prazos, comunicação com o consumidor e planejamento regional de entregas.
Como limitação, a base é anonimizada e não contém todos os detalhes operacionais da entrega, como transportadora, centro de distribuição ou motivo exato do atraso. Além disso, a análise é exploratória, não permitindo afirmar causalidade definitiva. Trabalhos futuros poderiam incluir modelos estatísticos preditivos, segmentação de clientes, análise textual dos comentários das avaliações e comparação mais detalhada entre regiões e categorias de produtos.