El Market Basket Analysis es una técnica en el ámbito de análisis y minería de datos en el campo del comercio. Su objetivo principal es descubrir patrones de asociación entre productos que seuelen ser comprados juntos por los clientes.
Las 3 métricas principales para evaluar reglas de asociación son:
Confidence (Confianza): Probabilidad de comprar sabiendo que se compró A. Ej. Pan –> Mantequilla 0.8 de cada 100 clientes que compraron pan, 80 compraron mantequilla también.
**Lift*(Elevación): Cuánto más probable es comprar B cuando se compra A en comparación dela probabilidad de comprar B sin saber si se compró A. Ej. Lift > 1 Compra A impulsa B. Lift = 1 No tienen relación de compra. Lift < 1 A reduce la compra de B. **Support* (Soporte): Popularidad del producto dentro de las transacciones. Ej. Pan y Mantequilla 0.05 El 5% de todas las transacciones compraron estos dos productos juntos.
Instacart es una plataforma de súper a domicilio en Estados Unidos.
La base de datos contiene más de 3 millones de órdenes de alrededor de
200,000 clientes, con casi 50,000 productos distintos organizados en
pasillos (aisles) y departamentos. La base viene partida en varios
archivos: el detalle de las órdenes está dividido en dos archivos
(order_products__prior.csv y
order_products__train.csv) que hay que
concatenar para tener todas las transacciones, y los
nombres de los productos, pasillos y departamentos vienen en catálogos
aparte que hay que pegar. El objetivo es limpiar la base de forma
estratégica y posteriormente aplicar MBA para descubrir patrones de
compra y diseñar promociones que aumenten las ventas.
#install.packages("tidyverse") # Paquete global para manipulación y análisis de datos
library (tidyverse)
#install.packages("janitor") # Examinar y limpiar bases de datos sucias
library(janitor)
#install.packages("Matrix") # Para trabajar con matrices
library(Matrix)
#install.packages("arules") # Genera reglas de asociación
library(arules)
#install.packages("arulesViz") #Visualizar reglas de asociación
library(arulesViz)
#install.packages("plyr")
library(plyr)
Aquí está la única diferencia de fondo contra el análisis de
abarrotes: el detalle de órdenes viene partido en dos archivos, así que
primero se concatenan con rbind() y
después se le pegan los catálogos.
#file.choose()
# Detalle de órdenes (viene partido en dos archivos)
prior <- read.csv("/Users/santiagojaramillo/Downloads/instacart/order_products__prior.csv")
train <- read.csv("/Users/santiagojaramillo/Downloads/instacart/order_products__train.csv")
# CONCATENAR los dos archivos de order_products
order_products <- rbind(prior, train)
rm(prior, train); gc()
## used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
## Ncells 2732070 146 8618012 460.3 NA 6089498 325.3
## Vcells 77723552 593 213999420 1632.7 16384 262488728 2002.7
nrow(order_products)
## [1] 33819106
# Catálogos
products <- read.csv("/Users/santiagojaramillo/Downloads/instacart/products.csv")
aisles <- read.csv("/Users/santiagojaramillo/Downloads/instacart/aisles.csv")
departments <- read.csv("/Users/santiagojaramillo/Downloads/instacart/departments.csv")
orders <- read.csv("/Users/santiagojaramillo/Downloads/instacart/orders.csv")
# Pegar catálogos para dejar una sola tabla plana (como venía "abarrotes")
products <- products %>%
left_join(aisles, by = "aisle_id") %>%
left_join(departments, by = "department_id")
df <- order_products %>%
left_join(products[, c("product_id","product_name","aisle","department")], by = "product_id") %>%
left_join(orders[, c("order_id","user_id","order_dow","order_hour_of_day","days_since_prior_order")],
by = "order_id")
rm(order_products); gc()
## used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
## Ncells 2795450 149.3 8618012 460.3 NA 6291555 336.1
## Vcells 276276276 2107.9 618871380 4721.7 16384 550247873 4198.1
summary(df)
## order_id product_id add_to_cart_order reordered
## Min. : 1 Min. : 1 Min. : 1.000 Min. :0.0000
## 1st Qu.: 855413 1st Qu.:13519 1st Qu.: 3.000 1st Qu.:0.0000
## Median :1710660 Median :25256 Median : 6.000 Median :1.0000
## Mean :1710566 Mean :25576 Mean : 8.368 Mean :0.5901
## 3rd Qu.:2565587 3rd Qu.:37935 3rd Qu.: 11.000 3rd Qu.:1.0000
## Max. :3421083 Max. :49688 Max. :145.000 Max. :1.0000
##
## product_name aisle department
## Length :33819106 Length :33819106 Length :33819106
## N.unique : 49685 N.unique : 134 N.unique : 21
## N.blank : 0 N.blank : 0 N.blank : 0
## Min.nchar: 3 Min.nchar: 3 Min.nchar: 4
## Max.nchar: 159 Max.nchar: 29 Max.nchar: 15
##
##
## user_id order_dow order_hour_of_day days_since_prior_order
## Min. : 1 Min. :0.000 Min. : 0.00 Min. : 0.00
## 1st Qu.: 51435 1st Qu.:1.000 1st Qu.:10.00 1st Qu.: 5.00
## Median :102626 Median :3.000 Median :13.00 Median : 8.00
## Mean :102944 Mean :2.737 Mean :13.43 Mean :11.36
## 3rd Qu.:154412 3rd Qu.:5.000 3rd Qu.:16.00 3rd Qu.:15.00
## Max. :206209 Max. :6.000 Max. :23.00 Max. :30.00
## NAs :2078068
str(df)
## 'data.frame': 33819106 obs. of 11 variables:
## $ order_id : int 2 2 2 2 2 2 2 2 2 3 ...
## $ product_id : int 33120 28985 9327 45918 30035 17794 40141 1819 43668 33754 ...
## $ add_to_cart_order : int 1 2 3 4 5 6 7 8 9 1 ...
## $ reordered : int 1 1 0 1 0 1 1 1 0 1 ...
## $ product_name : chr "Organic Egg Whites" "Michigan Organic Kale" "Garlic Powder" "Coconut Butter" ...
## $ aisle : chr "eggs" "fresh vegetables" "spices seasonings" "oils vinegars" ...
## $ department : chr "dairy eggs" "produce" "pantry" "pantry" ...
## $ user_id : int 202279 202279 202279 202279 202279 202279 202279 202279 202279 205970 ...
## $ order_dow : int 5 5 5 5 5 5 5 5 5 5 ...
## $ order_hour_of_day : int 9 9 9 9 9 9 9 9 9 17 ...
## $ days_since_prior_order: num 8 8 8 8 8 8 8 8 8 12 ...
#dplyr::count(df, order_id, sort = TRUE)
#dplyr::count(df, product_id, sort = TRUE)
#dplyr::count(df, product_name, sort = TRUE)
#dplyr::count(df, aisle, sort = TRUE)
#dplyr::count(df, department, sort = TRUE)
#dplyr::count(df, user_id, sort = TRUE)
#dplyr::count(df, order_dow, sort = TRUE)
#dplyr::count(df, order_hour_of_day, sort = TRUE)
#dplyr::count(df, add_to_cart_order, sort = TRUE)
#dplyr::count(df, reordered, sort = TRUE)
head(df,10)
## order_id product_id add_to_cart_order reordered
## 1 2 33120 1 1
## 2 2 28985 2 1
## 3 2 9327 3 0
## 4 2 45918 4 1
## 5 2 30035 5 0
## 6 2 17794 6 1
## 7 2 40141 7 1
## 8 2 1819 8 1
## 9 2 43668 9 0
## 10 3 33754 1 1
## product_name
## 1 Organic Egg Whites
## 2 Michigan Organic Kale
## 3 Garlic Powder
## 4 Coconut Butter
## 5 Natural Sweetener
## 6 Carrots
## 7 Original Unflavored Gelatine Mix
## 8 All Natural No Stir Creamy Almond Butter
## 9 Classic Blend Cole Slaw
## 10 Total 2% with Strawberry Lowfat Greek Strained Yogurt
## aisle department user_id order_dow order_hour_of_day
## 1 eggs dairy eggs 202279 5 9
## 2 fresh vegetables produce 202279 5 9
## 3 spices seasonings pantry 202279 5 9
## 4 oils vinegars pantry 202279 5 9
## 5 baking ingredients pantry 202279 5 9
## 6 fresh vegetables produce 202279 5 9
## 7 doughs gelatins bake mixes pantry 202279 5 9
## 8 spreads pantry 202279 5 9
## 9 packaged vegetables fruits produce 202279 5 9
## 10 yogurt dairy eggs 205970 5 17
## days_since_prior_order
## 1 8
## 2 8
## 3 8
## 4 8
## 5 8
## 6 8
## 7 8
## 8 8
## 9 8
## 10 12
tail(df,10)
## order_id product_id add_to_cart_order reordered
## 33819097 3421058 30316 6 1
## 33819098 3421058 35578 7 0
## 33819099 3421058 32650 8 1
## 33819100 3421063 49235 1 1
## 33819101 3421063 13565 2 1
## 33819102 3421063 14233 3 1
## 33819103 3421063 35548 4 1
## 33819104 3421070 35951 1 1
## 33819105 3421070 16953 2 1
## 33819106 3421070 4724 3 1
## product_name
## 33819097 Baby Brie
## 33819098 Genoa Salame with White Cheddar Cheese & Toasted Rounds Small Plates
## 33819099 White Giant Paper Towel Rolls
## 33819100 Organic Half & Half
## 33819101 No Salt Added Gluten-Free Blue Chips Made with Organic Blue Corn
## 33819102 Natural Artesian Water
## 33819103 Twice Baked Potatoes
## 33819104 Organic Unsweetened Almond Milk
## 33819105 Creamy Peanut Butter
## 33819106 Broccoli Florettes
## aisle department user_id order_dow
## 33819097 packaged cheese dairy eggs 136952 3
## 33819098 packaged meat meat seafood 136952 3
## 33819099 paper goods household 136952 3
## 33819100 cream dairy eggs 169679 0
## 33819101 chips pretzels snacks 169679 0
## 33819102 water seltzer sparkling water beverages 169679 0
## 33819103 prepared meals deli 169679 0
## 33819104 soy lactosefree dairy eggs 139822 6
## 33819105 spreads pantry 139822 6
## 33819106 packaged produce produce 139822 6
## order_hour_of_day days_since_prior_order
## 33819097 18 15
## 33819098 18 15
## 33819099 18 15
## 33819100 10 4
## 33819101 10 4
## 33819102 10 4
## 33819103 10 4
## 33819104 10 8
## 33819105 10 8
## 33819106 10 8
#Tabla de Departamento y Día de la semana
tabyl(df, department, order_dow)
## department 0 1 2 3 4 5 6
## alcohol 18150 21215 21002 23293 27100 28465 20069
## babies 85108 75243 58526 54229 53263 53529 58845
## bakery 236619 211500 158372 144756 143201 160173 170560
## beverages 455671 499815 385525 358740 354296 392118 358010
## breakfast 133781 134010 99432 90534 88797 98176 94339
## bulk 6444 6526 4728 4408 4150 4761 4915
## canned goods 239817 187010 138017 124363 120165 134754 170731
## dairy eggs 1070923 989872 733935 665526 660577 740339 769895
## deli 220115 191595 140718 126970 124428 140059 151655
## dry goods pasta 191654 153674 115554 102895 100015 108313 133235
## frozen 462177 374893 293978 277634 271295 303762 353119
## household 133690 126346 104634 99909 97848 104254 107971
## international 58723 48153 35759 32882 30848 34235 40555
## meat seafood 159221 123398 90113 81066 80098 91659 113683
## missing 15262 12442 9459 9233 9044 10512 11444
## other 6459 6234 5171 4840 4764 5293 5325
## pantry 370336 316582 250162 236784 234551 259372 289032
## personal care 85072 75268 62543 59855 57583 60673 67699
## pets 18039 15163 12383 13113 12922 14372 16229
## produce 2057869 1746944 1244389 1111911 1095847 1231232 1400186
## snacks 508562 555951 413960 375557 371904 410392 370086
#Tabla de Departamento y Recompra
tabyl(df, department, reordered)
## department 0 1
## alcohol 68302 90992
## babies 185290 253453
## bakery 455301 769880
## beverages 971223 1832952
## breakfast 324192 414877
## bulk 15196 20736
## canned goods 603540 511317
## dairy eggs 1857344 3773723
## deli 429309 666231
## dry goods pasta 486874 418466
## frozen 1068800 1268058
## household 462205 312447
## international 177217 103938
## meat seafood 318889 420349
## missing 46877 30519
## other 22583 15503
## pantry 1277020 679799
## personal care 317838 150855
## pets 40627 61594
## produce 3455782 6432596
## snacks 1279337 1727075
# Eliminar columnas
df <-subset(df, select=-c(days_since_prior_order))
# Eliminar renglones: órdenes de un solo producto no sirven para reglas de asociación
tam.orden <- dplyr::count(df, order_id)
df <- df[df$order_id %in% tam.orden$order_id[tam.orden$n > 1], ]
df <- distinct(df)
# Quitar espacios de más y las comas de los nombres de producto
# (la coma es el separador del basket, si se queda parte el producto en dos)
df$product_name <- str_squish(df$product_name)
df$product_name <- gsub("[,\"']", " ", df$product_name)
df$product_name <- str_squish(df$product_name)
df$order_dow <- factor(df$order_dow, levels = 0:6,
labels = c("Domingo","Lunes","Martes","Miercoles","Jueves","Viernes","Sabado"))
df$order_hour_of_day <- as.integer(df$order_hour_of_day)
df$reordered <- as.logical(df$reordered)
# Borrar todos los NA's
df <- df[!is.na(df$product_name), ]
#Reemplazar los NA´s con CEROS
#df[is.na(df)] <- 0
#Reemplazar los NA´s con el PROMEDIO
#df$altura[is.na(df.altura)] <- mean (df$altura, na.rn=TRUE)
boxplot(df$add_to_cart_order, horizontal = TRUE)
boxplot(tam.orden$n, horizontal = TRUE)
# Generar Basket
#Ordenar de menor a mayor la columna order_id
df <- df[order(df$order_id),]
#Generar Basket
basket <- ddply(df, c("order_id"), function(df) paste(df$product_name, collapse=","))
#Eliminar número de orden
basket$order_id <- NULL
#Cambiar el título de la columna V1 por Producto
colnames(basket)<-c("Producto")
# Exportar basket
write.csv(basket,"/Users/santiagojaramillo/Downloads/instacart/basket.csv",quote = FALSE, row.names=FALSE)
#file.choose()
tr <- read.transactions("/Users/santiagojaramillo/Downloads/instacart/basket.csv", sep=",")
## Warning in asMethod(object): removing duplicated items in transactions
reglas.asociacion <- apriori(tr,parameter=list(supp=0.001, conf=0.2,maxlen=10))
## Apriori
##
## Parameter specification:
## confidence minval smax arem aval originalSupport maxtime support minlen
## 0.2 0.1 1 none FALSE TRUE 5 0.001 1
## maxlen target ext
## 10 rules TRUE
##
## Algorithmic control:
## filter tree heap memopt load sort verbose
## 0.1 TRUE TRUE FALSE TRUE 2 TRUE
##
## Absolute minimum support count: 3182
##
## set item appearances ...[0 item(s)] done [0.00s].
## set transactions ...[49585 item(s), 3182491 transaction(s)] done [7.89s].
## sorting and recoding items ... [1853 item(s)] done [0.26s].
## creating transaction tree ... done [2.24s].
## checking subsets of size 1 2 3 4 done [0.68s].
## writing ... [845 rule(s)] done [0.00s].
## creating S4 object ... done [0.20s].
#summary (reglas.asociacion)
#inspect (reglas.asociacion)
reglas.asociacion <- sort(reglas.asociacion,by= "confidence", decreasing=TRUE)
#summary(reglas.asociacion)
#inspect(reglas.asociacion)
top10reglas <- head(reglas.asociacion, n=10, by="confidence")
plot(top10reglas,method="graph", engine="htmlwidget")
Las mismas 845 reglas se pueden ordenar de tres formas, y cada orden responde una pregunta distinta:
# Las 10 reglas donde es más seguro que también se lleven el segundo producto
inspect(head(reglas.asociacion, n=10, by="confidence"))
## lhs rhs support confidence coverage lift count
## [1] {Total 2% Lowfat Greek Strained Yogurt With Blueberry,
## Total 2% Lowfat Greek Strained Yogurt with Peach} => {Total 2% with Strawberry Lowfat Greek Strained Yogurt} 0.001201574 0.5918588 0.002030171 61.115686 3824
## [2] {Lime Sparkling Water,
## Sparkling Lemon Water} => {Sparkling Water Grapefruit} 0.001439753 0.5163399 0.002788382 20.842015 4582
## [3] {Zero Calorie Cola} => {Soda} 0.001290184 0.4966134 0.002597965 45.744366 4106
## [4] {Honeycrisp Apple,
## Strawberries} => {Banana} 0.001219799 0.4903992 0.002487360 3.190521 3882
## [5] {Organic Hass Avocado,
## Organic Raspberries,
## Organic Strawberries} => {Bag of Organic Bananas} 0.001274473 0.4902696 0.002599536 3.979784 4056
## [6] {Organic Fuji Apple,
## Strawberries} => {Banana} 0.001198432 0.4902314 0.002444626 3.189429 3814
## [7] {Sparkling Lemon Water,
## Sparkling Water Grapefruit} => {Lime Sparkling Water} 0.001439753 0.4796399 0.003001737 31.568219 4582
## [8] {Organic Hass Avocado,
## Organic Navel Orange} => {Bag of Organic Bananas} 0.001249022 0.4663851 0.002678091 3.785901 3975
## [9] {Total 2% Lowfat Greek Strained Yogurt with Peach,
## Total 2% with Strawberry Lowfat Greek Strained Yogurt} => {Total 2% Lowfat Greek Strained Yogurt With Blueberry} 0.001201574 0.4640214 0.002589481 69.051893 3824
## [10] {Cucumber Kirby,
## Organic Fuji Apple} => {Banana} 0.001059233 0.4628587 0.002288459 3.011343 3371
# Las 10 reglas donde un producto más empuja al otro
inspect(head(reglas.asociacion, n=10, by="lift"))
## lhs rhs support confidence coverage lift count
## [1] {Kiwi Sandia Sparkling Water} => {Blackberry Cucumber Sparkling Water} 0.001034724 0.3432711 0.003014305 93.21308 3293
## [2] {Blackberry Cucumber Sparkling Water} => {Kiwi Sandia Sparkling Water} 0.001034724 0.2809727 0.003682650 93.21308 3293
## [3] {Lemon Sparkling Water} => {Grapefruit Sparkling Water} 0.001093169 0.3513786 0.003111085 72.44489 3479
## [4] {Grapefruit Sparkling Water} => {Lemon Sparkling Water} 0.001093169 0.2253822 0.004850289 72.44489 3479
## [5] {Non Fat Acai & Mixed Berries Yogurt} => {Icelandic Style Skyr Blueberry Non-fat Yogurt} 0.001286414 0.4513284 0.002850283 72.00464 4094
## [6] {Icelandic Style Skyr Blueberry Non-fat Yogurt} => {Non Fat Acai & Mixed Berries Yogurt} 0.001286414 0.2052336 0.006268046 72.00464 4094
## [7] {Icelandic Style Skyr Blueberry Non-fat Yogurt} => {Non Fat Raspberry Yogurt} 0.002339048 0.3731702 0.006268046 70.11518 7444
## [8] {Non Fat Raspberry Yogurt} => {Icelandic Style Skyr Blueberry Non-fat Yogurt} 0.002339048 0.4394852 0.005322246 70.11518 7444
## [9] {Total 2% Lowfat Greek Strained Yogurt with Peach,
## Total 2% with Strawberry Lowfat Greek Strained Yogurt} => {Total 2% Lowfat Greek Strained Yogurt With Blueberry} 0.001201574 0.4640214 0.002589481 69.05189 3824
## [10] {Nonfat Icelandic Style Strawberry Yogurt} => {Icelandic Style Skyr Blueberry Non-fat Yogurt} 0.001480601 0.4275862 0.003462696 68.21683 4712
# Las 10 combinaciones que más se repiten
inspect(head(reglas.asociacion, n=10, by="support"))
## lhs rhs support confidence
## [1] {Organic Hass Avocado} => {Bag of Organic Bananas} 0.02034915 0.2941569
## [2] {Organic Strawberries} => {Bag of Organic Bananas} 0.02033062 0.2357807
## [3] {Organic Strawberries} => {Banana} 0.01832841 0.2125605
## [4] {Organic Avocado} => {Banana} 0.01747405 0.3027388
## [5] {Organic Baby Spinach} => {Banana} 0.01677774 0.2131528
## [6] {Organic Baby Spinach} => {Bag of Organic Bananas} 0.01653045 0.2100111
## [7] {Strawberries} => {Banana} 0.01356799 0.2909939
## [8] {Large Lemon} => {Banana} 0.01352337 0.2683385
## [9] {Organic Raspberries} => {Bag of Organic Bananas} 0.01328613 0.2972965
## [10] {Organic Raspberries} => {Organic Strawberries} 0.01116515 0.2498365
## coverage lift count
## [1] 0.06917789 2.387832 64761
## [2] 0.08622680 1.913960 64702
## [3] 0.08622680 1.382911 58330
## [4] 0.05771988 1.969608 55611
## [5] 0.07871224 1.386765 53395
## [6] 0.07871224 1.704775 52608
## [7] 0.04662637 1.893196 43180
## [8] 0.05039669 1.745801 43038
## [9] 0.04468984 2.413318 42283
## [10] 0.04468984 2.897435 35533
# Los 15 productos que más se venden
itemFrequencyPlot(tr, topN=15, type="relative", col="steelblue",
main="Los 15 productos que más se compran")
El plátano está en el 15% de las compras y el plátano orgánico en el 12%. Ya se vende solo. Ponerlo en promoción es regalar dinero en algo que la gente iba a comprar de todos modos.
Lo que sí conviene es usarlo de gancho: ponerlo cerca de lo que sí se quiere mover. Casi todas las reglas más comunes terminan en plátano, o sea que es el producto por donde pasa todo el mundo.
Esta es la relación más fuerte de toda la base y no tiene nada que ver con el plátano.
Quien compra agua mineral de kiwi con sandía se lleva la de zarzamora con pepino 93 veces más seguido que un cliente cualquiera. Lo mismo pasa con la de limón y la de toronja (72 veces más), y con los yogures: quien compra el de arándano se lleva el de frambuesa 70 veces más seguido.
La regla más segura de todas es esa: de los que compran yogur griego de arándano y de durazno, 6 de cada 10 también se llevan el de fresa.
O sea que la gente no compra “un yogur”, compra la colección de sabores.
Qué hacer: armar paquetes de sabores surtidos, o promociones de “lleva 3 sabores”. Es lo más fácil de vender porque el cliente ya lo estaba haciendo solo.
Los 15 productos más vendidos son casi todos fruta y verdura, y la mayoría orgánica: plátano, fresa, espinaca baby, aguacate hass, limón, frambuesa.
Y se compran entre ellos: quien lleva aguacate hass, frambuesa y fresa orgánicos, casi la mitad se lleva también plátano orgánico.
Qué hacer: una canasta armada de fruta y verdura orgánica de la semana. El cliente ya está juntando esos mismos productos uno por uno.
Esta es la parte que más se presta a confusión y vale la pena separarla bien:
Para promociones sirven las de lift alto, no las de support alto. Juntar dos cosas que ya se venden solas no aumenta nada.
De las 845 reglas, 432 son de dos productos y 405 de tres. De cuatro productos nada más salieron 8.
O sea que no hay evidencia para armar promociones de cuatro cosas juntas. Lo que sí aguanta la base son combos de dos, y con cuidado de tres.
Con supp=0.001 salieron 845 reglas, que es un número que
sí se puede revisar a mano. Si se le baja más van a salir miles de
reglas de combinaciones que pasaron dos o tres veces por casualidad, y
ahí ya no se distingue lo que sirve de lo que es puro ruido.