Aula 4 - Análise de Vendas em R

Data de Publicação

14 de setembro de 2026

Motivação

Na Aula 3 você respondeu, em Python, a uma pergunta de negócio de verdade: quais produtos, clientes, países e períodos priorizar, e o quanto cancelamentos e devoluções custam ao resultado. O raciocínio que sustentou aquela análise, isolar vendas válidas, agregar por pedido e por produto, medir o que foi perdido, não pertence ao pandas. Ele pertence a quem analisa dados, e pode ser expresso em qualquer ferramenta capaz de ler uma tabela.

R é essa outra ferramenta. Ela já apareceu de passagem na Aula 1, como linguagem relevante em estatística e na academia, e agora é hora de conhecê-la na prática, através do tidyverse, uma coleção de pacotes que oferece para o R o mesmo tipo de sintaxe expressiva e encadeada que o pandas oferece para o Python. Este roteiro reproduz, passo a passo, a mesma análise da Aula 3, e depois vai além dela: apresenta perguntas de negócio novas, que o enunciado original não pedia, para mostrar que o valor de dominar duas ferramentas não é redundância, é alcance.

Objetivos de aprendizagem

Ao final deste roteiro, você será capaz de:

  1. Reconhecer as equivalências entre pandas e tidyverse: groupby().agg() e group_by() |> summarise(), filtros booleanos e filter(), pd.to_datetime e as funções do pacote lubridate;
  2. Reproduzir em R os indicadores de negócio calculados em Python na Aula 3: faturamento, ticket médio, produtos mais relevantes e impacto de cancelamentos;
  3. Escrever pipelines tidyverse (|>) para responder a perguntas de negócio que ainda não haviam sido formuladas, usando o mesmo dataset;
  4. Comparar criticamente os resultados das duas linguagens sobre os mesmos dados, como forma de validar a tradução.
Como usar este roteiro

Este roteiro pressupõe o projeto-vendas das Aulas 1 a 3 funcionando, com o Online Retail já baixado em data/. Assim como a Aula 3, este script não é a análise completa: é uma tradução de referência, seção a seção, mais uma seção nova de perguntas extras. Ao final, quatro perguntas ajudam a testar se os critérios usados aqui, os mesmos da Aula 3, continuam sólidos quando expressos em outra linguagem.

Configurando o ambiente R

A Aula 1 isolou o ambiente Python por projeto com uv venv, e registrou versões exatas em requirements.txt. Aqui simplificamos deliberadamente: instalamos os pacotes uma vez, de forma global na máquina, sem isolar por projeto.

install.packages(c("tidyverse", "readxl", "lubridate"))
Uma simplificação, não um esquecimento

R tem um equivalente ao uv venv: o pacote renv, que cria um ambiente isolado e reprodutível por projeto, com um renv.lock no lugar do requirements.txt. Ele fica fora do escopo desta aula, cujo foco é a tradução da análise, mas vale saber que ele existe: se um dia você depender de R em um projeto sério, renv é o próximo passo natural, pela mesma razão que uv venv foi o da Aula 1.

1. Retomando o projeto

Como sempre, comece verificando onde o projeto parou:


cd ./projeto-vendas

# Onde paramos? O git log conta a historia ate aqui:
git log --oneline

# E o estado atual? Espera-se uma area de trabalho limpa:
git status
bbe6150 Adiciona pandas e registra dependencias em requirements.txt
1b4999b Adiciona .gitignore para ambiente, dados e segredos
2ce3f11 Adiciona README com a descricao do projeto
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   README.md
    modified:   requirements.txt

no changes added to commit (use "git add" and/or "git commit -a")

Se o log mostra os commits das Aulas 1 a 3 e o status diz que não há nada a commitar, o ambiente e os dados estão prontos. Os dados já baixados na Aula 2 servem sem qualquer alteração.

2. O enunciado

O enunciado é exatamente o mesmo da Aula 3: o gestor de vendas precisa entender o desempenho comercial do último ano e identificar oportunidades para aumentar o faturamento, considerando:

  • Evolução mensal do faturamento e sazonalidade;
  • Produtos com maior faturamento, volume e frequência de compra;
  • Países e clientes mais relevantes;
  • Valor médio dos pedidos;
  • Impacto financeiro de cancelamentos e devoluções;
  • Outliers e problemas de qualidade dos dados;
  • Recomendações práticas para a gestão comercial.

O que muda aqui não é a pergunta, é a ferramenta usada para respondê-la, e o quanto ela permite ir além do que foi pedido.

3. Preparando os dados

O carregamento e a preparação replicam exatamente a Aula 3: a mesma coluna de receita por item e o mesmo sinalizador de cancelamento, derivado do prefixo C no número da fatura.

library(readxl)
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.0     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.2     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)

data_path <- file.path("projeto-vendas", "data", "online_retail.xlsx")

retail <- read_excel(data_path) |>
  rename(
    invoice_no = InvoiceNo,
    stock_code = StockCode,
    description = Description,
    quantity = Quantity,
    invoice_date = InvoiceDate,
    unit_price = UnitPrice,
    customer_id = CustomerID,
    country = Country
  ) |>
  mutate(
    invoice_date = as.POSIXct(invoice_date),
    revenue = quantity * unit_price,
    is_cancelled = str_starts(str_to_upper(invoice_no), "C")
  )

Os nomes das colunas são padronizados em snake_case, convenção adotada no restante do código. Com a base carregada, confirmamos suas dimensões:

cat(
  "Arquivo carregado com sucesso.\n",
  glue::glue("Dimensão da base: {nrow(retail)} linhas x {ncol(retail)} colunas\n"),
  sep = ""
)
Arquivo carregado com sucesso.
Dimensão da base: 541909 linhas x 10 colunas

4. Visão geral da base

Antes de qualquer indicador de negócio, o mesmo retrato geral tirado na Aula 3: tamanho, tipos e uma primeira olhada nos números. Estrutura e tipos de dados:

glimpse(retail)
Rows: 541,909
Columns: 10
$ invoice_no   <chr> "536365", "536365", "536365", "536365", "536365", "536365…
$ stock_code   <chr> "85123A", "71053", "84406B", "84029G", "84029E", "22752",…
$ description  <chr> "WHITE HANGING HEART T-LIGHT HOLDER", "WHITE METAL LANTER…
$ quantity     <dbl> 6, 6, 8, 6, 6, 2, 6, 6, 6, 32, 6, 6, 8, 6, 6, 3, 2, 3, 3,…
$ invoice_date <dttm> 2010-12-01 08:26:00, 2010-12-01 08:26:00, 2010-12-01 08:…
$ unit_price   <dbl> 2.55, 3.39, 2.75, 3.39, 3.39, 7.65, 4.25, 1.85, 1.85, 1.6…
$ customer_id  <dbl> 17850, 17850, 17850, 17850, 17850, 17850, 17850, 17850, 1…
$ country      <chr> "United Kingdom", "United Kingdom", "United Kingdom", "Un…
$ revenue      <dbl> 15.30, 20.34, 22.00, 20.34, 20.34, 15.30, 25.50, 11.10, 1…
$ is_cancelled <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, F…

Primeiras linhas:

retail |>
  slice_head(n = 6)
# A tibble: 6 × 10
  invoice_no stock_code description      quantity invoice_date        unit_price
  <chr>      <chr>      <chr>               <dbl> <dttm>                   <dbl>
1 536365     85123A     WHITE HANGING H…        6 2010-12-01 08:26:00       2.55
2 536365     71053      WHITE METAL LAN…        6 2010-12-01 08:26:00       3.39
3 536365     84406B     CREAM CUPID HEA…        8 2010-12-01 08:26:00       2.75
4 536365     84029G     KNITTED UNION F…        6 2010-12-01 08:26:00       3.39
5 536365     84029E     RED WOOLLY HOTT…        6 2010-12-01 08:26:00       3.39
6 536365     22752      SET 7 BABUSHKA …        2 2010-12-01 08:26:00       7.65
# ℹ 4 more variables: customer_id <dbl>, country <chr>, revenue <dbl>,
#   is_cancelled <lgl>

Estatísticas das variáveis numéricas:

retail |>
  select(quantity, unit_price, revenue) |>
  summary()
    quantity            unit_price            revenue          
 Min.   :-80995.000   Min.   :-11062.060   Min.   :-168469.60  
 1st Qu.:     1.000   1st Qu.:     1.250   1st Qu.:      3.40  
 Median :     3.000   Median :     2.080   Median :      9.75  
 Mean   :     9.552   Mean   :     4.611   Mean   :     17.99  
 3rd Qu.:    10.000   3rd Qu.:     4.130   3rd Qu.:     17.40  
 Max.   : 80995.000   Max.   : 38970.000   Max.   : 168469.60  

5. Qualidade dos dados

Refaça também o diagnóstico da Aula 3, consolidado em um único quadro: ausentes, duplicatas e a contagem dos suspeitos já conhecidos.

Valores ausentes, coluna a coluna:

quality <- tibble(
  coluna = names(retail),
  valores_ausentes = map_int(retail, \(column) sum(is.na(column))),
  percentual_ausente = map_dbl(retail, \(column) mean(is.na(column)) * 100)
) |>
  arrange(desc(percentual_ausente)) |>
  mutate(percentual_ausente = round(percentual_ausente, 2))

quality
# A tibble: 10 × 3
   coluna       valores_ausentes percentual_ausente
   <chr>                   <int>              <dbl>
 1 customer_id            135080              24.9 
 2 description              1454               0.27
 3 invoice_no                  0               0   
 4 stock_code                  0               0   
 5 quantity                    0               0   
 6 invoice_date                0               0   
 7 unit_price                  0               0   
 8 country                     0               0   
 9 revenue                     0               0   
10 is_cancelled                0               0   

Duplicatas, considerando somente as colunas originais:

original_columns <- c(
  "invoice_no", "stock_code", "description", "quantity",
  "invoice_date", "unit_price", "customer_id", "country"
)

duplicate_count <- retail |>
  select(all_of(original_columns)) |>
  duplicated() |>
  sum()

cat(glue::glue(
  "Linhas exatamente duplicadas: {scales::comma(duplicate_count)}\n",
  "Percentual de duplicatas: {scales::number(duplicate_count / nrow(retail), accuracy = 0.0001, scale = 100, suffix = '%')}\n"
))
Linhas exatamente duplicadas: 5,268
Percentual de duplicatas: 0.9721%

Registros potencialmente problemáticos:

issues <- tibble(
  metrica = c(
    "Faturas canceladas", "Quantidade negativa", "Quantidade igual a zero",
    "Preço negativo", "Preço igual a zero", "Descrição ausente",
    "Cliente não identificado", "Data inválida ou ausente"
  ),
  quantidade = c(
    sum(retail$is_cancelled),
    sum(retail$quantity < 0),
    sum(retail$quantity == 0),
    sum(retail$unit_price < 0),
    sum(retail$unit_price == 0),
    sum(is.na(retail$description)),
    sum(is.na(retail$customer_id)),
    sum(is.na(retail$invoice_date))
  )
)

issues
# A tibble: 8 × 2
  metrica                  quantidade
  <chr>                         <int>
1 Faturas canceladas             9288
2 Quantidade negativa           10624
3 Quantidade igual a zero           0
4 Preço negativo                    2
5 Preço igual a zero             2515
6 Descrição ausente              1454
7 Cliente não identificado     135080
8 Data inválida ou ausente          0

Valores extremos, quantidade e preço unitário:

percentiles <- c(0.01, 0.25, 0.50, 0.75, 0.99)

retail |>
  summarise(
    quantidade = list(round(quantile(quantity, percentiles, na.rm = TRUE), 2)),
    preco_unitario = list(round(quantile(unit_price, percentiles, na.rm = TRUE), 2))
  ) |>
  pivot_longer(everything(), names_to = "variavel", values_to = "valores") |>
  unnest_wider(valores, names_sep = "_")
# A tibble: 2 × 6
  variavel  `valores_1%` `valores_25%` `valores_50%` `valores_75%` `valores_99%`
  <chr>            <dbl>         <dbl>         <dbl>         <dbl>         <dbl>
1 quantida…        -2             1             3            10              100
2 preco_un…         0.19          1.25          2.08          4.13            18

6. Isolando as vendas válidas

O mesmo corte definido na Aula 3: exclui cancelamentos e linhas com quantidade ou preço não positivos.

sales <- retail |>
  filter(!is_cancelled, quantity > 0, unit_price > 0)

sales_summary <- sales |>
  summarise(
    linhas = n(),
    pedidos = n_distinct(invoice_no),
    clientes = n_distinct(customer_id, na.rm = TRUE),
    unidades = sum(quantity),
    faturamento_bruto = sum(revenue)
  )

sales_summary
# A tibble: 1 × 5
  linhas pedidos clientes unidades faturamento_bruto
   <int>   <int>    <int>    <dbl>             <dbl>
1 530104   19960     4338  5588376         10666685.

7. Indicadores de pedidos, produtos, países e clientes

Com as vendas válidas isoladas, podemos mudar a unidade de análise conforme a pergunta: pedidos, produtos, países ou clientes. Cada tabela abaixo tem uma linha por entidade analisada e reúne as métricas necessárias para responder ao enunciado.

Valor médio dos pedidos

Primeiro, agregamos os itens de uma mesma fatura. O resultado terá uma linha por pedido, permitindo calcular seu valor médio sem dar mais peso aos pedidos que possuem muitos itens.

orders <- sales |>
1  group_by(invoice_no) |>
  summarise(
    order_date = min(invoice_date),
    customer_id = first(customer_id),
    country = first(country),
    item_quantity = sum(quantity),
    order_value = sum(revenue),
    .groups = "drop"
2  )
1
Cada grupo representa uma fatura, portanto uma linha da tabela resultante representa um pedido.
2
summarise() documenta como cada coluna do pedido é obtida a partir das linhas de itens.
order_value_summary <- orders |>
  summarise(
    ticket_medio = mean(order_value),
    ticket_mediano = median(order_value)
  )

order_value_summary
# A tibble: 1 × 2
  ticket_medio ticket_mediano
         <dbl>          <dbl>
1         534.           304.

Distribuição do valor dos pedidos:

orders |>
  reframe(
    percentil = c(25, 50, 75, 90, 99),
    valor_pedido = round(quantile(order_value, percentil / 100), 2)
  )
# A tibble: 5 × 2
  percentil valor_pedido
      <dbl>        <dbl>
1        25         153.
2        50         304.
3        75         496.
4        90         941.
5        99        4821.

O ticket médio responde diretamente à pergunta sobre o valor médio dos pedidos. A mediana e os percentis complementam a resposta: se a média estiver muito acima da mediana, poucos pedidos de alto valor estão puxando o indicador para cima.

Produtos com maior faturamento, volume e frequência

Para comparar produtos, usamos três métricas diferentes:

  • revenue: faturamento gerado;
  • units_sold: unidades vendidas;
  • order_count: quantidade de pedidos distintos em que o produto apareceu, nossa medida de frequência de compra.
products <- sales |>
  filter(!is.na(description)) |>
  group_by(stock_code, description) |>
  summarise(
    units_sold = sum(quantity),
    revenue = sum(revenue),
    order_count = n_distinct(invoice_no),
    .groups = "drop"
  )

Produtos com maior faturamento:

products |>
  slice_max(revenue, n = 10, with_ties = FALSE)
# A tibble: 10 × 5
   stock_code description                        units_sold revenue order_count
   <chr>      <chr>                                   <dbl>   <dbl>       <int>
 1 DOT        DOTCOM POSTAGE                            706 206249.         706
 2 22423      REGENCY CAKESTAND 3 TIER                13879 174485.        1988
 3 23843      PAPER CRAFT , LITTLE BIRDIE             80995 168470.           1
 4 85123A     WHITE HANGING HEART T-LIGHT HOLDER      37599 104340.        2189
 5 47566      PARTY BUNTING                           18295  99504.        1685
 6 85099B     JUMBO BAG RED RETROSPOT                 48474  94340.        2089
 7 23166      MEDIUM CERAMIC TOP STORAGE JAR          78033  81701.         247
 8 M          Manual                                   7224  78110.         289
 9 POST       POSTAGE                                  3150  78102.        1126
10 23084      RABBIT NIGHT LIGHT                      30788  66965.         994

Produtos com maior volume vendido:

products |>
  slice_max(units_sold, n = 10, with_ties = FALSE)
# A tibble: 10 × 5
   stock_code description                        units_sold revenue order_count
   <chr>      <chr>                                   <dbl>   <dbl>       <int>
 1 23843      PAPER CRAFT , LITTLE BIRDIE             80995 168470.           1
 2 23166      MEDIUM CERAMIC TOP STORAGE JAR          78033  81701.         247
 3 84077      WORLD WAR 2 GLIDERS ASSTD DESIGNS       55047  13842.         535
 4 85099B     JUMBO BAG RED RETROSPOT                 48474  94340.        2089
 5 85123A     WHITE HANGING HEART T-LIGHT HOLDER      37599 104340.        2189
 6 22197      POPCORN HOLDER                          36761  34299.         803
 7 84879      ASSORTED COLOUR BIRD ORNAMENT           36461  59095.        1455
 8 21212      PACK OF 72 RETROSPOT CAKE CASES         36419  21259.        1320
 9 23084      RABBIT NIGHT LIGHT                      30788  66965.         994
10 22492      MINI PAINT SET VINTAGE                  26633  16938.         380

Produtos com maior frequência de compra:

products |>
  slice_max(order_count, n = 10, with_ties = FALSE)
# A tibble: 10 × 5
   stock_code description                        units_sold revenue order_count
   <chr>      <chr>                                   <dbl>   <dbl>       <int>
 1 85123A     WHITE HANGING HEART T-LIGHT HOLDER      37599 104340.        2189
 2 85099B     JUMBO BAG RED RETROSPOT                 48474  94340.        2089
 3 22423      REGENCY CAKESTAND 3 TIER                13879 174485.        1988
 4 47566      PARTY BUNTING                           18295  99504.        1685
 5 20725      LUNCH BAG RED RETROSPOT                 19353  35752.        1564
 6 84879      ASSORTED COLOUR BIRD ORNAMENT           36461  59095.        1455
 7 22720      SET OF 3 CAKE TINS PANTRY DESIGN         7493  38158.        1385
 8 21212      PACK OF 72 RETROSPOT CAKE CASES         36419  21259.        1320
 9 20727      LUNCH BAG  BLACK SKULL.                 12240  22421.        1273
10 22457      NATURAL SLATE HEART CHALKBOARD           9172  28148.        1249

Os três rankings não são necessariamente iguais. Um produto pode aparecer em muitos pedidos, mas com poucas unidades por pedido, enquanto outro pode ter grande volume concentrado em poucas compras de atacado.

Países mais relevantes para o negócio

A relevância de um país é medida por faturamento, quantidade de pedidos e número de clientes identificados. O faturamento ordena a tabela, enquanto as demais colunas ajudam a distinguir mercados amplos de vendas concentradas em poucos compradores.

countries <- sales |>
  group_by(country) |>
  summarise(
    revenue = sum(revenue),
    order_count = n_distinct(invoice_no),
    customer_count = n_distinct(customer_id, na.rm = TRUE),
    units_sold = sum(quantity),
    .groups = "drop"
  )

top_countries <- countries |>
  slice_max(revenue, n = 10, with_ties = FALSE)

top_countries
# A tibble: 10 × 5
   country         revenue order_count customer_count units_sold
   <chr>             <dbl>       <int>          <int>      <dbl>
 1 United Kingdom 9025222.       18019           3920    4662390
 2 Netherlands     285446.          94              9     200361
 3 EIRE            283454.         288              3     147173
 4 Germany         228867.         457             94     119261
 5 France          209715.         392             87     112103
 6 Australia       138521.          57              9      83901
 7 Spain            61577.          90             30      27940
 8 Switzerland      57090.          54             21      30629
 9 Belgium          41196.          98             25      23237
10 Sweden           38378.          36              8      36083

Clientes mais relevantes para o negócio

Como registros sem customer_id não podem ser atribuídos a uma pessoa específica, eles são excluídos somente desta agregação. Isso não os remove das demais análises de vendas válidas.

identified_sales <- sales |>
  filter(!is.na(customer_id))

customers <- identified_sales |>
  group_by(customer_id) |>
  summarise(
    revenue = sum(revenue),
    order_count = n_distinct(invoice_no),
    units_sold = sum(quantity),
    country = first(country),
    .groups = "drop"
  )

top_customers <- customers |>
  slice_max(revenue, n = 10, with_ties = FALSE)

top_customers
# A tibble: 10 × 5
   customer_id revenue order_count units_sold country       
         <dbl>   <dbl>       <int>      <dbl> <chr>         
 1       14646 280206.          73     196915 Netherlands   
 2       18102 259657.          60      64124 United Kingdom
 3       17450 194551.          46      69993 United Kingdom
 4       16446 168472.           2      80997 United Kingdom
 5       14911 143825.         201      80265 EIRE          
 6       12415 124915.          21      77374 Australia     
 7       14156 117380.          55      57885 EIRE          
 8       17511  91062.          31      64549 United Kingdom
 9       16029  81025.          63      40208 United Kingdom
10       12346  77184.           1      74215 United Kingdom

8. Cancelamentos e devoluções

O lado espelhado das vendas válidas: tudo o que foi excluído do faturamento bruto calculado na seção 6.

cancellations <- retail |>
  filter(is_cancelled | quantity < 0)

cancellation_summary <- retail |>
  summarise(
    faturas_canceladas = n_distinct(invoice_no[is_cancelled]),
    linhas_quantidade_negativa = sum(quantity < 0),
    valor_devolucoes_ajustes = abs(sum(revenue[revenue < 0])),
    receita_liquida = sum(revenue)
  )

cancellation_summary
# A tibble: 1 × 4
  faturas_canceladas linhas_quantidade_negativa valor_devolucoes_ajustes
               <int>                      <int>                    <dbl>
1               3836                      10624                  918937.
# ℹ 1 more variable: receita_liquida <dbl>

9. Indo além: novas perguntas de negócio

O enunciado da Aula 3 não pedia isto, mas o mesmo dataset permite outras perguntas de gestão. Três exemplos, cada um resolvido com um pipeline tidyverse sobre as vendas válidas isoladas na seção 6.

Produtos mais vendidos por trimestre, no Reino Unido

A maior parte das vendas vem do Reino Unido, sede da empresa. Faz sentido perguntar: quais produtos lideram em unidades vendidas, trimestre a trimestre, só nesse mercado?

uk_quarterly_top_products <- sales |>
  filter(country == "United Kingdom") |>
  mutate(quarter = paste0(year(invoice_date), "-Q", quarter(invoice_date))) |>
  group_by(quarter, stock_code, description) |>
  summarise(
    units_sold = sum(quantity),
    revenue = sum(revenue),
    .groups = "drop"
  ) |>
  slice_max(units_sold, n = 5, by = quarter, with_ties = FALSE) |>
  arrange(quarter, desc(units_sold))

uk_quarterly_top_products
# A tibble: 25 × 5
   quarter stock_code description                        units_sold revenue
   <chr>   <chr>      <chr>                                   <dbl>   <dbl>
 1 2010-Q4 84077      WORLD WAR 2 GLIDERS ASSTD DESIGNS        4571    993.
 2 2010-Q4 85123A     WHITE HANGING HEART T-LIGHT HOLDER       3633  10110.
 3 2010-Q4 22834      HAND WARMER BABUSHKA DESIGN              3322   4482.
 4 2010-Q4 21212      PACK OF 72 RETROSPOT CAKE CASES          3266   2266.
 5 2010-Q4 22693      GROW A FLYTRAP OR SUNFLOWER IN TIN       2617   2509.
 6 2011-Q1 23166      MEDIUM CERAMIC TOP STORAGE JAR          74215  77184.
 7 2011-Q1 85099B     JUMBO BAG RED RETROSPOT                 10224  19241.
 8 2011-Q1 85123A     WHITE HANGING HEART T-LIGHT HOLDER       8874  24541.
 9 2011-Q1 84077      WORLD WAR 2 GLIDERS ASSTD DESIGNS        8609   2067.
10 2011-Q1 17003      BROCADE RING PURSE                       6903   1484.
# ℹ 15 more rows

country == "United Kingdom" só funciona porque conferimos antes o rótulo exato do país nos dados; a Aula 2 já ensinou a não supor esse tipo de valor sem checar.

Concentração de receita: quantos clientes sustentam o faturamento?

Uma leitura comum em gestão comercial é a curva de Pareto: uma fração pequena de clientes costuma responder por boa parte da receita. Vale checar se isso se confirma aqui.

customer_revenue <- customers |>
  select(customer_id, revenue) |>
  arrange(desc(revenue)) |>
  mutate(
    customer_rank = row_number(),
    cumulative_share = cumsum(revenue) / sum(revenue)
  )

customers_for_80pct <- customer_revenue |>
  filter(cumulative_share >= 0.8) |>
  slice_head(n = 1) |>
  pull(customer_rank)

total_customers <- nrow(customer_revenue)

cat(glue::glue(
  "{customers_for_80pct} de {total_customers} clientes ",
  "({scales::percent(customers_for_80pct / total_customers, accuracy = 0.1)}) ",
  "respondem por 80% do faturamento das vendas válidas.\n"
))
1133 de 4338 clientes (26.1%) respondem por 80% do faturamento das vendas válidas.

Clientes com maior faturamento:

customer_revenue |>
  slice_head(n = 10)
# A tibble: 10 × 4
   customer_id revenue customer_rank cumulative_share
         <dbl>   <dbl>         <int>            <dbl>
 1       14646 280206.             1           0.0314
 2       18102 259657.             2           0.0606
 3       17450 194551.             3           0.0824
 4       16446 168472.             4           0.101 
 5       14911 143825.             5           0.117 
 6       12415 124915.             6           0.131 
 7       14156 117380.             7           0.145 
 8       17511  91062.             8           0.155 
 9       16029  81025.             9           0.164 
10       12346  77184.            10           0.173 

Sazonalidade por dia da semana

A seção 4 já apontou sazonalidade mensal como pergunta em aberto. Antes de chegar lá, um recorte mais fino: existe um dia da semana melhor para vendas?

weekday_revenue <- sales |>
  mutate(weekday = wday(invoice_date, label = TRUE, abbr = FALSE, week_start = 1)) |>
  group_by(weekday) |>
  summarise(
    orders = n_distinct(invoice_no),
    revenue = sum(revenue),
    avg_revenue_per_order = revenue / orders,
    .groups = "drop"
  ) |>
  arrange(weekday)

weekday_revenue
# A tibble: 6 × 4
  weekday   orders  revenue avg_revenue_per_order
  <ord>      <int>    <dbl>                 <dbl>
1 Monday      3126 1779575.                  569.
2 Tuesday     3554 2178633.                  613.
3 Wednesday   3690 1851148.                  502.
4 Thursday    4246 2203161.                  519.
5 Friday      3140 1840340.                  586.
6 Sunday      2204  813828.                  369.

Vale notar o que falta nos dados: se um dia da semana não aparece na tabela acima, é porque não há faturas registradas nele, o que já é, por si só, um achado sobre a operação.

Recapitulando o que este roteiro entrega:

  • Reproduziu em R e tidyverse toda a análise da Aula 3: visão geral, qualidade dos dados, vendas válidas, valor médio dos pedidos, rankings de produtos, países e clientes, e impacto de cancelamentos;
  • Foi além do enunciado original com três perguntas novas: produtos líderes por trimestre no Reino Unido, concentração de receita por cliente e sazonalidade por dia da semana.

O que ainda falta é o mesmo que faltava na Aula 3: sazonalidade mensal completa, investigação direta dos outliers e as recomendações práticas para a gestão comercial.

Para casa

  1. Complete, no seu notebook, os pontos que este roteiro ainda não cobre: sazonalidade mensal completa e a investigação direta dos outliers levantados nas perguntas acima;
  2. Escreva, em células de texto, os critérios que você usou para definir venda válida, cancelamento e registro anômalo, e compare com as respostas que você deu na Aula 3: mudaram?
  3. Encerre com pelo menos três recomendações práticas para a gestão comercial, apoiadas nos números que você calculou;
  4. Antes de considerar o notebook pronto, rode a partir de uma sessão R limpa (Restart R and Run All Chunks, o equivalente R ao Restart Kernel and Run All), e só então commite no projeto-vendas com uma mensagem descritiva.

A ferramenta muda. O raciocínio que você defende sobre os dados, não.