Esta é a parte 2 de um dos projetos de conclusão do módulo Business Analytics da Formação Cientista de Dados da Data Science Academy (DSA), e pode ser considerada uma extensão da análise exploratória que eu fiz na parte 1. A parte 1 você encontra nesse link:
https://midoritoyota.github.io/Market_Basket_Analysis_1.html
Nessa primeira fase farei a análise exploratória dos dados de um aplicativo de compras para entender o perfil de compra dos usuários.
“The Instacart Online Grocery Shopping Dataset 2017”, Accessed from:
https://www.instacart.com/datasets/grocery-shopping-2017 on 25/05/2020.
Post do VP da Instacart definindo o problema em detalhes:
https://tech.instacart.com/3-million-instacart-orders-open-sourced-d40d29ead6f2?gi=41f7b19cd164
Utilizarei o pacole arules, que cria as regras segundo o algorítimo APRIORI e o pacote arulesViz, que permite a visualização de objetos da classe transactions.
# Carregando pacotes
library("arules")
library("arulesViz")
library("tidyverse")
library("knitr")
library("lubridate")
library("plyr")
library("RColorBrewer")
library("repr")
library("sqldf")
library("treemap")
# Carregado datasets
orders = read.csv("data/orders.csv", sep = ",")
products = read.csv("data/products.csv", sep = ",")
aisles = read.csv("data/aisles.csv", sep = ",")
departments = read.csv("data/departments.csv", sep = ",")
train = read.csv("data/order_products__train.csv", sep = ",")
Para transformar um dataframe em um objeto do tipo transactions é necessário criar uma coluna onde todos os produtos de uma mesma compra estarão em uma mesma linha, apenas separados por vírgula. Sendo assim:
# Inserir o nome do produto nos dados de treino
data = merge(train, products, by = "product_id")
# Remover colunas desnecessárias
data = subset(data, select = -c(product_id, reordered, aisle_id, department_id))
# Trasnformar factors em characters
data$product_name = as.character(data$product_name)
# Remover vírgulas e ponto e vírgula do nome dos itens (se houver)
data$product_name = gsub(',', '-', data$product_name)
data$product_name = gsub(';', '-', data$product_name)
# Colocando itens em ordem de compra (importante não deixar em ordem aleatória!)
data = data[order(data$order_id, data$add_to_cart_order),]
head(data)
# Agrupar os dados por Order id e colapsar produtos
transacoes = ddply(data, "order_id", function(order) paste(order$product_name, collapse = ","))
# Remover coluna order_id (só usada para agrupar)
transacoes$order_id = NULL
head(transacoes)
# Transformar itens para formato "transactions" (arquivo próprio do algorítimo)
write.csv(transacoes,"./data/transacoes.csv", quote = FALSE, row.names = FALSE)
data_trans = read.transactions('./data/transacoes.csv', format = 'basket', sep=',')
# Classe do objeto
class(data_trans)
O pacote APRIORI lê um tipo diferente de arquivo. O arquivo é da classe transactions, e não dá para visualizar com head, pois não é um dataframe nem uma matriz.
O comando inspect
pertence ao pacote arulesViz e é uma forma de visualizar o conteúdo de um objeto da classe transactions. Abaixo, vemos a aplicação do comando inspect
, que mostra um índice identificador da ordem de compra e dentro de cada ordem de compra há uma lista de itens comprados, como se fossem os itens de um cupom fiscal. Lembre-se de utilizar o comando head
antes de utilizar o inspect nos dados ou seu código irá tentar mostrar todos os itens e irá travar!
inspect(head(data_trans,3))
O objeto da classe transaction é dividido em 3 componentes @data
, @itemInfo
e @itemsetInfo
:
@data
é uma matriz que contém 2 listas: data_trans@data@i (lista completa dos itens comprados do primeiro ao último cliente, por código do produto) e data_trans@data@p (lista com os índices que dividem uma compra de outra). Dim e Dinames são apenas informações sobre a natureza do objeto.@itemInfo
é um dataframe com os labels (que tem o nome de cada produto).@itemsetInfo
é uma dataframe vazio que só será preenchido quando rodarmos a função apriori# Estrutura da Classe transaction
str(data_trans)
Podemos visualizar o conteúdo do objeto se quisermos:
# Visualizar nome dos produtos
data_trans@itemInfo$labels[1:10]
# Resumo do arquivo
summary(data_trans)
O resumo acima nos dá algumas informações:
# Plot dos itens mais frequentemente comprados
options(repr.plot.width=12, repr.plot.height=7)
itemFrequencyPlot(data_trans, topN=15, type="absolute", col=brewer.pal(8,'Set3'), main="Absolute Item Frequency Plot")
Na análise exploratória da parte 1, vimos que existe uma variedade enorme de alimentos não saudáveis como chocolates e sorvetes. Olhando a frequência de compra dos itens acima, percebemos que os itens mais comprados são os saudáveis. Talvez isso ocorra porque como temos uma variedade enorme de tipos de sorvete, o volume de compras se dilui para cada item. No caso, se for para comprar bananas só tem Bananas (ou Bag of organic bananas). Podemos confirmar essa hitpótese mais tarde através de um gráfico.
Para criar regras é necessários definir 2 parâmetros:
supp
) : é o quanto um determinado set de compras apareceu na nossa amostra (em porcentagem)conf
) : é o quanto a regra aconteceu em todas as vezes que ela podia ter acontecido.O maxlen
é opcional. Colocamos para evitar regras muito longas.
# Criando regras
rules = apriori(data_trans, parameter = list(supp=0.0001, conf=0.70, maxlen=10), control = list(verbose=FALSE))
# Resumo das regras
summary(rules)
o resumo das regras acima nos mostra que foram criadas 297 regras, em sua maioria de 4 itens.
# Visualizar as 10 primeiras regras
options(digits=2)
rules = sort(rules, by="confidence", decreasing=TRUE)
inspect(rules[1:10])
Foram criadas as regras acima que mostram o que as pessoas costumam comprar costumam comprar em um mesmo pedido.
Com as regras criadas podemos responder a duas perguntas principais:
# Criação de regra para o item Blueberry (lhs = Blueberry)
blueberry_rules = apriori(data_trans, parameter = list(supp=0.0001, conf=0.50), appearance = list(lhs="Blueberry", default="rhs"), control = list(verbose=F))
inspect(blueberry_rules)
Pessoas que compram Blueberry compra também... Bananas Claro! Banana é vida rsrs
Também podemos fazer da maneira inversa:
# Criação da regra para o item METAL (rhs = Organic Strawberries)
strawberry_rules = apriori(data_trans, parameter = list(supp=0.0001, conf=0.80), appearance = list(rhs="Organic Strawberries", default="lhs"), control = list(verbose=F))
inspect(strawberry_rules)
Backyard Barbeque Potato Chips, um pouco de comida industrializada, finalmente!
Você lembra que haviam 1246 tipos diferentes de chocolate no dataset? Chegamos nesse número na parte 1 do projeto. Pois bem, vamos ver está sendo comprado de chocolate em maior quantidade:
# Filtrando os 5 item de chocolate mais comprados
sqldf(" SELECT product_name, COUNT(*) AS qtd
FROM data
WHERE product_name LIKE '%chocolate%'
GROUP BY product_name
ORDER BY qtd DESC LIMIT 5 ")
Vou verificar então o que as pessoas compram antes de comprar Chocolate Ice Cream.
# Criação da regra para o item METAL (rhs = Organic Strawberries)
chocolate_rules = apriori(data_trans, parameter = list(supp=0.00002, conf=0.7), appearance = list(rhs="Chocolate Ice Cream", default="lhs"), control = list(verbose=F))
inspect(chocolate_rules)
Com esse tanto de informação, fica um pouco difícil de visualizar as regras. Podemos melhorar isso usando um plot.
O pacote ArulesViz permite fazer alguns plots especificamente para visualizarmos as regras criadas. Vejamos algumas:
# Plot default
options(repr.plot.width=12, repr.plot.height=12)
plot(chocolate_rules, method="graph", engine = "default", main = "Cholocate Ice Cream Rules")
Dá para perceber que a compra de outros tipos de sorvete, levam as pessoas a comprarem sorvete de chocolate!
Podemos fazer o plot utilizando diferentes tipos de engine
, vamos ver como fica o plot do strawberry_rules com o graphviz engine
.
# Plot com Engine Graphviz
options(repr.plot.width=12, repr.plot.height=8)
plot(strawberry_rules, method="graph", engine = "graphviz", main = "Strawberry Rules")
O plot mais legal, na minha opinião, é com o engine
= "interactive" pois cria um gráfico interativo onde você pode selecionar os itens com o mouse, porém, ele é aberto em uma janela fora do jupyter-notebook, então não inseri aqui mas fica a dica.
Podemos fazer o mesmo plot treemap que fizemos na parte 1, mas agora, utilizando a quantidade comprada como peso para os produtos:
# Inserir o nome do produto nos dados de treino
data2 = merge(train, products, by = "product_id")
data2 = merge(data2, aisles, by = "aisle_id")
data2 = merge(data2, departments, by = "department_id")
head(data2)
# Preparo dos dados para visualização do treemap
options(repr.plot.width=7, repr.plot.height=7)
tmp = sqldf( " SELECT department, aisle, COUNT(product_id) AS n_products
FROM data2
GROUP BY department, aisle " )
# Plot treemap
treemap(tmp,index=c("department","aisle"),vSize="n_products",palette="Set2",border.col="#FFFFFF", title = "")
Pelo treemap acima, percebemos que a teorida de que mais alimentos industrializados estão sendo comprados porém como eles estão vidividos em produtos diferentes (diferentes tipos de sorvete etc), eles não aparecem nos produtos mais vendidos não está correta, pode ser em parte verdade, mas, alimentos como verduras e frutas frescas ainda são as mais compradas.
O APRIORI é um ótimo algorítimo para ser usado na análise exploratória. Ele dá um panorama bom do padrão de consumo dos clientes e é possível obter insights para melhorar as recomendações ao usuário ou melhorar a proximidade das categorias. A dificuldade está em calibrar os parâmetros para cada produto, o que não é nada difícil, mas um pouco trablhoso.
Nos vemos na parte 3!