library(dplyr)
library(lubridate)
library(ggplot2)
library(tidyr)
library(padr)
library(zoo)
library(plotly)
library(rfm)
library(readxl)
library(data.table)
library(glue)
library(scales)
library(gridExtra)
library(treemap)
library(rfm)
library(GGally)
library(highcharter)
library(stringr)
library(arules)
Pada projek Study Case ini, digunakan data dari UCI Machine Learning yang berupa dataset transaksi yang terjadi pada periode 01/12/2010 hingga 09/12/2011. Dataset ini merupakan transaksi yang terjadi pada online retail yang berbasis di UK.
Berdasarkan informasi dari UCI Machine Learning, dataset Online Retail, dataset ini tidak memiliki NA Value
, terdiri dari 541.909 baris dengan 8 kolom, serta berikut informasi setiap atributnya:
Variable | Description |
---|---|
InvoiceNo | a 6-digit integral number uniquely assigned to each transaction. If this code starts with letter ‘c’, it indicates a cancellation |
StockCode | a 5-digit integral number uniquely assigned to each distinct product |
Description | Product (item) name |
Quantity | the quantities of each product (item) per transaction |
InvoiceDate | the day and time when each transaction was generated |
UnitPrice | product price per unit |
CustomerID | a 5-digit integral number uniquely assigned to each customer |
Country | the name of the country where each customer resides |
Jumlah baris serta kolom pada dataset telah sesuai dengan yang termuat pada sumber data. Selanjutnya, mari kita coba eksplor secara sekilas dataset ini terlebih dahulu.
#> Rows: 541,909
#> Columns: 8
#> $ InvoiceNo <chr> "536365", "536365", "536365", "536365", "536365", "536365"~
#> $ StockCode <chr> "85123A", "71053", "84406B", "84029G", "84029E", "22752", ~
#> $ Description <chr> "WHITE HANGING HEART T-LIGHT HOLDER", "WHITE METAL LANTERN~
#> $ Quantity <dbl> 6, 6, 8, 6, 6, 2, 6, 6, 6, 32, 6, 6, 8, 6, 6, 3, 2, 3, 3, ~
#> $ InvoiceDate <dttm> 2010-12-01 08:26:00, 2010-12-01 08:26:00, 2010-12-01 08:2~
#> $ UnitPrice <dbl> 2.55, 3.39, 2.75, 3.39, 3.39, 7.65, 4.25, 1.85, 1.85, 1.69~
#> $ CustomerID <dbl> 17850, 17850, 17850, 17850, 17850, 17850, 17850, 17850, 17~
#> $ Country <chr> "United Kingdom", "United Kingdom", "United Kingdom", "Uni~
Tiap kolom dari dataset telah memiliki tipe data yang tepat, namun, nanti saya akan melakukan pengecekan pada kolom dengan tipe data karakter. Tujuannya, saya akan menentukan terkait konversi tipe data kolom ke factor
.
#> InvoiceNo StockCode Description Quantity
#> Length:541909 Length:541909 Length:541909 Min. :-80995.00
#> Class :character Class :character Class :character 1st Qu.: 1.00
#> Mode :character Mode :character Mode :character Median : 3.00
#> Mean : 9.55
#> 3rd Qu.: 10.00
#> Max. : 80995.00
#>
#> InvoiceDate UnitPrice CustomerID
#> Min. :2010-12-01 08:26:00.00 Min. :-11062.06 Min. :12346
#> 1st Qu.:2011-03-28 11:34:00.00 1st Qu.: 1.25 1st Qu.:13953
#> Median :2011-07-19 17:17:00.00 Median : 2.08 Median :15152
#> Mean :2011-07-04 13:34:57.16 Mean : 4.61 Mean :15288
#> 3rd Qu.:2011-10-19 11:27:00.00 3rd Qu.: 4.13 3rd Qu.:16791
#> Max. :2011-12-09 12:50:00.00 Max. : 38970.00 Max. :18287
#> NA's :135080
#> Country
#> Length:541909
#> Class :character
#> Mode :character
#>
#>
#>
#>
Pada ringkasan dari dataset secara kesuluruhan, NA Value
ditemukan ada, tepatnya pada kolom CustomerID. Hal ini berarti kondisi dataset tidak sesuai dengan deskripsi dari sumber data. Kemudian, nilai negatif ditemukan pada kolom Quantity, adapun kolom ini menunjukkan jumlah item yang dibeli. Sehingga, temuan ini cukup aneh dan perlu inpeksi secara mendetail pada dataset ini. Adapun deskripsi terkait rentang periode dataset ini telah sesuai dengan kondisi sebenarnya.
Langkah berikutnya, saya akan melakukan Exploratory Data Analysis (EDA) dan preparasi data.
Mari kita cek jumlah nilai unik pada tiap kolom data yang bertipe character.
# Melakukan pengecakan jumlah nilai unik dari setiap kolom yang bertipe data "character"
# Prosedur pengecekan dengan Looping
df_raw <- as.data.frame(df_raw)
char_col <- df_raw %>% select_if(is.character) %>% colnames()
for (i in char_col){
cat("kolom",i,"memiliki nilai unik:", as.character(length(unique(df_raw[,i]))), "(", as.character(round(length(unique(df_raw[,i]))/nrow(df_raw)*100, 2)), "% dari total data )","\n")
}
#> kolom InvoiceNo memiliki nilai unik: 25900 ( 4.78 % dari total data )
#> kolom StockCode memiliki nilai unik: 4070 ( 0.75 % dari total data )
#> kolom Description memiliki nilai unik: 4212 ( 0.78 % dari total data )
#> kolom Country memiliki nilai unik: 38 ( 0.01 % dari total data )
Dari hasil inspeksi, saya memilih kolom Country dirubah menjadi tipe data factor karena memiliki jumlah nilai unik Country sedikit.
#> InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice
#> 0 0 1454 0 0 0
#> CustomerID Country
#> 135080 0
Dataset ini ternyata memiliki NA Value
pada kolom Description
dan CustomerID
. Saya akan mencoba mencari tahu lebih mendalam pada dua kolom tersebut, yang mana dengan baris berupa NA Value
.
Customer ID
Sebanyak 135.080 baris pada dataset ini memiliki nilai NA Value
pada kolon CustomerID. Saya tidak dapat membuang data ini secara langusung karena saya berasumsi bisa jadi ini adalah pembelian tanpa mendaftar akun terlebih dahulu atau sebagianya. Hal yang pasti ialah online retail tetap mendapatkan pendapatan. Sehingga, baris pada dataset dengan nilai NA Value
tetap dapat memberikan pemahaman akan berbagai hal, misal total pendapatan yang didapat perusahaan, kesedian stok barang, dan lain-lain.
Description
Secara sekilas, kita melihat bahwa semua kolom Description yang berupa NA Value
memiliki kolom CustomerID juga berupa NA Value
.
Ternyata memang benar, semua kolom Description yang berupa NA Value
memiliki kolom CustomerID juga berupa NA Value
.
Begitu juga dengan kolom UnitPrice, di mana semua kolom Description yang berupa NA Value
memiliki nilai UnitPrice bernilai 0.
Imputasi NA Value
Pada sesi imputasi NA Value
, hal ini hanya dapat dilakukan pada kolom Description, dan itu pun kita harus meninjaunya terlebih dahulu. Kolom Description dapat dilakukan imputasi karena adakalanya NA Value
terjadi pada item dengan StockCode yang sama dengan salah satu baris namun memliki nilai Description yang tidak berupa NA Value
.
# Mengambil semua kode StockCode yang memiliki kolom Description berupa NA Value
na_desc <- unique(df_raw[is.na(df_raw$Description) == T,]$StockCode)
# Meninjau terkait tersedianya deskripsi dari StockCode yang kolom Description-nya berupa NA Value pada baris di dataset
df_raw %>%
filter(StockCode %in% na_desc, is.na(Description) == F) %>%
select(StockCode, Description) %>%
distinct()
Sebagaimana dugaan saya sebelumnya, bahwa besar kemungkinan StockCode dengan Description berupa NA Value
sebenarnya memiliki Description pada baris lain. Namun, dari inspeksi di atas, kita dapat melihat bahwa terdapat beberapa item yang memiliki Description lebih dari satu.
df_desc_count <- df_raw %>%
filter(StockCode %in% na_desc, is.na(Description) == F) %>%
select(StockCode, Description) %>%
group_by(StockCode, Description) %>%
summarise(freq = n()) %>% ungroup()
df_desc_count %>%
group_by(StockCode) %>%
summarise(Description = unique(Description), freq = freq, DescriptionCount = n()) %>%
arrange(-DescriptionCount, -freq)
Ternyata memang benar bahwa cukup banyak StockCode yang memiliki Description lebih dari satu.
Selanjutnya, mari kita pilih Description yang paling sering muncul untuk tiap satu StockCode yang digunakan imputasi. Kemudian, dilanjutkan dengan imputasi pada kolom Description.
# Memilih Description yang paling sering muncul untuk tiap satu StockCode
stockcode_na_desc <- df_desc_count %>%
group_by(StockCode) %>%
summarise(Description = unique(Description), freq = freq, DescriptionCount = n()) %>%
arrange(-DescriptionCount, -freq) %>% ungroup() %>%
group_by(StockCode) %>%
summarise(max_freq = max(freq),
DescriptionCount = unique(DescriptionCount),
Description = Description
) %>%
arrange(-DescriptionCount) %>% slice(1) %>%
arrange(-DescriptionCount) %>% ungroup() %>%
select(StockCode, Description)
df_raw <- df_raw %>%
left_join(stockcode_na_desc, by = "StockCode") %>%
mutate(Description = ifelse(is.na(Description.x), Description.y, Description.x)) %>%
select(-Description.y, -Description.x) %>%
select(InvoiceNo, StockCode, Description, everything())
Selanjutnya, mari kita lakukan eksplorasi secara mendalam pada tiap kolomnya dan lakukan pembersihan data yang tidak relevan.
Cancel Order Sebagaimana yang disebutkan dalam deskripsi dari kolom dataset, transaksi diawali huruf C maka artinya transaksi tersebut dicancel.
#> [1] 1947
Terdapat 1.947 transaksi yang dibatalkan atau cancel.
Semua cancel transaction memiliki Quantity bernilai negatif. Hal ini cukup susah dipahami karena tidak terdapat petunjuk lain dari data ini terkait nilai Quantity yang negatif. Begitu juga adanya keterkaitan antara transaksi yang dibatalkan dengan nilai Quantity yang negatif, masih belum bisa didefinisikan dengan baik.
Invalid Berdasarkan deskripsi tiap kolom, InvoiceNo terdiri dari 6 digit angka yang bersifat unik setiap transaksi, maka kita perlu mempertimbangkan InvoiceNo yang tidak sesuai dengan kriteria tersebut.
Pada dataset ini, hanya ada tiga InvoiceNo yang tidak valid dan akan dibuang.
Deskripsi kolom menyatakan bahwa StockCode ini merupakan kode produk yang terdiri dari 5 digit angka yang bersifat unik. Namun, pada beberapa eksplorasi yang saya lakukan sebelumnya, beberapa nilai StockCode tidak sesuai dengan kriteria tersebut.
df_raw %>%
mutate(stock_code = toupper(StockCode)) %>%
mutate(StockCode_nchar = nchar(StockCode)) %>%
group_by(StockCode_nchar) %>%
summarise(freq = length(unique(StockCode))) %>%
arrange(StockCode_nchar)
Ternyata, banyak nilai StockCode yang tidak tersusun dari 5 digit angka. Mari kita lakukan cek dan pembersihan.
StockCode yang memiliki 1 sampai 4 karakter
df_raw %>%
mutate(StockCode_nchar = nchar(StockCode)) %>%
filter(StockCode_nchar %in% c(1,2,3,4)) %>%
select(StockCode, Description) %>% distinct()
Dapat dipastikan bahwa data StockCode yang tersusun atas 1 hingga 4 karakter adalah InvoiceNo yang tidak valid.
StockCode yang memiliki 5 karakter
df_raw %>%
mutate(StockCode_nchar = nchar(StockCode)) %>%
filter(StockCode_nchar == 5) %>%
select(StockCode, Description) %>% distinct()
Data StockCode yang tersusun atas 5 digit angka adalah data yang valid.
StockCode yang memiliki 6 karakter
df_raw %>%
mutate(StockCode_nchar = nchar(StockCode)) %>%
filter(StockCode_nchar == 6) %>%
select(StockCode, Description) %>% distinct()
StockCode yang terdiri dari 6 karakter itu tersusun atas 5 angka dan 1 huruf. Berdasarkan data di atas, huruf pada stock code terlihat seperti menunjukan karakter masing-masing produk. Sebagaimana contohnya pada StockCode 84997B dan 84997C itu adalah produk yang sama namun memiliki warna yang berbeda. sehingga, seluruh data di atas dapat dianggap sebagai produk yang berbeda dan valid.
StockCode yang memiliki lebih dari 6 karakter
df_raw %>%
mutate(StockCode_nchar = nchar(StockCode)) %>%
filter(StockCode_nchar > 6) %>%
select(StockCode, Description) %>% distinct()
Pada data yang memiliki StockCode lebih dari 6 karakter, berdasarkan nilai Description-nya, terdapat beberapa data yang menunjukkan bahwa produk tersebut valid. Namun, banyak data yang menunjukkan produk tidak valid
Pembersihan Data
df_raw <- df_raw %>%
mutate(StockCode = toupper(StockCode)) %>%
mutate(StockCode_nchar = nchar(StockCode)) %>%
filter(!StockCode_nchar %in% c(1,2,3,4)) %>%
filter(!StockCode%in% c("AMAZONFEE", "BANK CHARGES", "GIFT_0001_10","GIFT_0001_20","GIFT_0001_20", "GIFT_0001_30",
"GIFT_0001_40","GIFT_0001_50")) %>%
filter(!Description %in% c("ebay", "Unsaleable, destroyed")) %>%
select(-StockCode_nchar)
Berdasarkan pengecekan cancel transaction sebelumnya, kita dapatkan bahwa semua cancel transaction memiliki nilai Quantity negatif. Sehingga, mari kita lihat pada transaksi yang tidak dibatalkan namun memiliki nilai Quantity negatif.
#> [1] 1331
Sebanyak 1.331 baris dan 1.331 transaksi pada data ini memiliki nilai Quantity negatif, artinya Quantity bernilai negatif itu terdapat dalam satu InvoiceNo atau transaksi.
Kemudian, secara sekilas data di atas menunjukkan bahwa semunya memiliki UnitPrice 0 dan CustomerID berupa NA Value
.
Question <- c("Apakah semua Quantity < 0 dan bukan cancel trans. memiliki UnitPrice = 0?",
"Apakah semua Quantity < 0 dan bukan cancel trans. nilai CustomerID adalah NA?")
Answer <- c(nrow(df_raw %>% filter(Quantity < 0 & !grepl("C", df_raw$InvoiceNo) & UnitPrice != 0)) == 0, nrow(df_raw %>% filter(Quantity < 0 & !grepl("C", df_raw$InvoiceNo) & is.na(CustomerID) == T)) == 0)
data.frame(Question, Answer)
Adakalanya nilai Quantity negatif dan bukan cancel trans. adalah bukan transaksi sebenarnya, namun bisa jadi semacam pendataan stock atau yang lainnya. Hal ini hanya praduga saja. Akan tetapi, jika itu benar maka pada dataset ini akan dapat ditemukan baris dengan nilai UnitPrice == 0, Customer ID == NA
dan hanya pada satu InvoiceNo == satu baris itu memiliki nilai Quantity yang positif. Mari kita coba saja.
Sebelumnya, kita buang data yang memiliki nilai Quantity negatif dan bukan termasuk cancel trans. karena memang tidak ditemukan makna sebenarnya dari pemilik dataset ini.
Mari kita coba lihat baris dengan UnitPrice ==0 dan CustomerID == NA
.
dan kita lanjutkan dengan pengecekan keunikan nilai InvoiceNo-nya pada tiap transaksinya.
df_raw %>% filter(UnitPrice == 0 & is.na(CustomerID) == T) %>%
group_by(InvoiceNo) %>% summarise(freq = n()) %>% arrange(-freq)
Ternyata pada UnitPrice == 0 dan CustomerID == NA
itu juga terjadi dalam satu InvoiceNo dalam beberapa baris, sehingga masih belum ditemukan apa makna sebenarnya dari Quantity < 0. Namun, kita dapat membuang baris dengan UnitPrice == 0 dan CustomerID == NA
karena alasan mempertahankan baris dengan CustomerID == NA
sebelumnya, tepatnya pada eksplorasi data bagian Na Value Checking, adalah untuk memperhitungkan adanya kemungkinan pendapatan dari baris dengan CustomerID == NA
. Hal itu dimaksudkan agar tahu pendapatan sebenarnya Online Retail ini.
Mari kita identifikasi deskripsi yang tidak valid berdasarkan penelitian yang dilakukan oleh Diego Usai pada tanggal 14 Maret 2019 terkait Analisis Berbasis Pasar menggunakan data penjualan ritel online ini serta beberapa deskripsi tidak valid yang kita temukan bersama sebelumnya.
descr <- c( "check", "check?", "?", "??", "damaged", "found",
"adjustment", "Amazon", "AMAZON", "amazon adjust",
"Amazon Adjustment", "amazon sales", "Found", "FOUND",
"found box", "Found by jackie ","Found in w/hse","dotcom",
"dotcom adjust", "allocate stock for dotcom orders ta", "FBA",
"Dotcomgiftshop Gift Voucher £100.00", "on cargo order",
"wrongly sold (22719) barcode", "wrongly marked 23343",
"dotcomstock", "rcvd be air temp fix for dotcom sit",
"Manual", "John Lewis", "had been put aside",
"for online retail orders", "taig adjust", "amazon",
"incorrectly credited C550456 see 47", "returned",
"wrongly coded 20713", "came coded as 20713",
"add stock to allocate online orders", "Adjust bad debt",
"alan hodge cant mamage this section", "website fixed",
"did a credit and did not tick ret", "michel oops",
"incorrectly credited C550456 see 47", "mailout", "test",
"Sale error", "Lighthouse Trading zero invc incorr", "SAMPLES",
"Marked as 23343", "wrongly coded 23343","Adjustment",
"rcvd be air temp fix for dotcom sit", "Had been put aside.", "ebay",
"Unsaleable, destroyed" )
df_raw %>%
filter(Description %in% descr) %>% arrange(StockCode)
Ternyata setelah beberapa pengolahan atau preparasi data yang kita lakukan sebelumnya, data kita telah memiliki deskripsi yang valid.
Sekarang mari kita coba cek kembali pada data yand memiliki nilai Description == NA
Ternyata data kita yand memiliki nilai Description == NA
telah bersih.
Selanjutnya, kita masih perlu memastikan apakah tiap stock code telah memiliki deskripsi yang unik alias setiap StockCode merepsentasikan satu jenis item atau produk.
data_frame(
StockCode_unique = df_raw$StockCode %>% unique() %>% length(),
Description_unique = df_raw$Description %>% unique() %>% length(),
stock_description_unique = df_raw %>% select(StockCode, Description) %>% distinct() %>% nrow()
)
Data ditas mengindikasikan ada beberapa StockCode yang memiliki deskripsi lebih dari satu, karena seharusnya jumlah unik dari StockCode = Description = stock_code_description, maka kita perlu membersihkannya. Adapun dalam prosedur pembersihan dilakukan dengan menggunakan deskripsi yang paling akhi dari satu StockCode yang memiliki beberapa Deskription, dengan asumsi bahwa adanya pembaruan deskripsi.
df_products <- df_raw %>%
arrange(desc(InvoiceDate)) %>% #to get the last named of stock code and description
select(StockCode, Description) %>%
distinct()
df_products %>%
group_by(StockCode) %>%
summarise(DescriptionCount=n()) %>%
ungroup() %>%
filter(DescriptionCount > 1)
Ada 210 produk yang memiliki deskripsi lebih dari satu.
df_products <- df_products %>%
group_by(StockCode) %>%
slice(1)
df_raw <- df_raw %>% left_join(df_products , by="StockCode") %>%
mutate(Description = Description.y) %>%
select(-Description.x,-Description.y)
data_frame(
StockCode_unique = df_raw$StockCode %>% unique() %>% length(),
Description_unique = df_raw$Description %>% unique() %>% length(),
stock_description_unique = df_raw %>% select(StockCode, Description) %>% distinct() %>% nrow()
)
df_description <- df_raw %>%
arrange(desc(InvoiceDate)) %>% #to get the last named of stock code and description
select(StockCode, Description) %>%
distinct()
df_description <- df_description %>%
group_by(Description) %>%
slice(1)
df_raw <- df_raw %>% left_join(df_description , by= "Description") %>%
mutate(StockCode = StockCode.y) %>%
select(-StockCode.x,-StockCode.y)
data_frame(
StockCode_unique = df_raw$StockCode %>% unique() %>% length(),
Description_unique = df_raw$Description %>% unique() %>% length(),
stock_description_unique = df_raw %>% select(StockCode, Description) %>% distinct() %>% nrow()
)
Tiap produk pada online ritel ini telah memiliki satu deskripsi yang unik.
Berdasarkan deskripsi atribut, dinyatakan bahwa kolom CustomerID tersusun atas 5 digit dengan tipe data integer.
#> [1] 0
Pada data kita tidak terdapat baris dengan nilai CustomerID yang tidak valid atau sesuai dengan deskripsi, alias semua datanya valid berdasarkan kolom CustomerID.
Atribut data menyatakan bahwa kolom Country merupakan nama negara tempat tinggal setiap pelanggan, maka ada kalanya seorang pelanggan yang sama dalam riwayat transaksi ini berada dibeberapa negara. Coba kita cek.
data_frame(
CustomerUniq = df_raw %>% select(CustomerID ) %>% distinct() %>% nrow(),
CustomerCountryUniq = df_raw %>% select(CustomerID,Country) %>% distinct() %>% nrow()
)
Ternyata terdapat pelanggan yang pernah melakukan transaksi dibeberapa negara.
Data transaksi yang tersedia mencakup periode dari 1 Desember 2010 hingga 9 Desember 2011. Karena data transaksi Desember 2011 tidak lengkap, saya memutuskan untuk menghilangkan data tersebut dari analisis ini. Dengan demikian, kita memiliki data transaksi lengkap selama satu tahun, mulai dari Desember 2010 hingga November 2011.
Saya di sini akan melakukan pengecekan terkait adanya nilai duplikat dan membuangnya.
#> [1] TRUE
#> [1] 4954
Ternyata, terdapat 4.954 data yang merupakan duplikat. Mari kita buang nilai duplikat tersebut.
Sekarang mari kita lakukan beberapa feature extraction.
max_date = max(df_raw$InvoiceDate)
df_ready <- df_raw %>%
mutate(TotalAmount = Quantity*UnitPrice)
df_profile <- df_raw %>% mutate(TotalAmount = Quantity*UnitPrice) %>%
group_by(CustomerID) %>%
summarise(AvgAmountOrder = median(TotalAmount),
FirstOrder = min(InvoiceDate),
LastOrder = max(InvoiceDate)) %>%
mutate(AgeCustomer = round(difftime(time2 = FirstOrder, time1 = max_date, units = "days")+1)) %>%
filter(!is.na(CustomerID) == T) %>% ungroup()
df_profile2 <- df_raw %>% group_by(CustomerID, InvoiceNo) %>%
summarise(freq = n()) %>% ungroup() %>%
group_by(CustomerID) %>%
summarise(
AvgBaskets = round(median(freq),0)
) %>% ungroup() %>% filter(!is.na(CustomerID) == T)
df_ready <- df_ready %>%
left_join(df_profile, by = "CustomerID") %>%
left_join(df_profile2, by = "CustomerID")
df_ready
Beriku deskripsi beberapa feature yang saya buat.
Feature | Description |
---|---|
FirstOrder | Pertama kali customer melakukan transaksi |
LastOrder | Terakhir kali customer melakukan transaksi |
AvgAmountOrder | Rata-rata biaya yang dikeluarkan customer per-transaksi |
AvgBaskets | Rata-rata jumlah produk yang dibeli customer per-transaksi |
AgeCustomer | Umur Pelanggan |
plot_monthly_revenue <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
group_by(Month_Year) %>%
summarise(TotalRevenue = sum(TotalAmount)) %>%
mutate(label = paste0(as.character(round(TotalRevenue/1000, 2)), "K"),
popup = glue("{Month_Year}
Total Revenue: £ {label}")) %>%
ggplot(aes(x = Month_Year, y = TotalRevenue)) +
geom_point(aes(text = popup), size =3, colour = "darkblue")+
geom_line(linewidth = 1) +
# geom_point(aes(text = popup)) +
geom_point(data = . %>% filter(TotalRevenue == min(TotalRevenue)),
aes(x = Month_Year, y = TotalRevenue),
size = 4.5, colour = "darkred")+
geom_point(data = . %>% filter(TotalRevenue == max(TotalRevenue)),
aes(x = Month_Year, y = TotalRevenue),
size = 4.5, colour = "#eaec42")+
labs(title = "Monthly Total Revenue Online Retail",
x = NULL,
y = NULL) +
scale_y_continuous(labels = dollar_format(prefix = "£ ")) +
theme_minimal() +
theme(plot.title = element_text(size = 15))
ggplotly(plot_monthly_revenue, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Berdasarkan tren pendapatan bulanan, ritel online UK ini memiliki tren yang positif di 4 bulan terakhir dan fluktuatif di periode awal. Pendapatan terendah terjadi pada bulan Februari 2011 dengan nilai sekitar £ 442.290. Sedangkan pendapatan tertinggi terjadi di bulan Desember 2012 dengan nilai sekitar £ 1.134.010.
Selanjutnya, mari kita coba lihat Month-over-Month Growth atau MoM Growth dari pendapatan yang diperoleh ritel online UK.
calculate_mom_growth <- function(data) {
data$Month_Year <- as.yearmon(data$Month_Year)
data <- data[order(data$Month_Year), ]
data$MoM_Growth <- c(NA, (data$TotalRevenue[-1] - data$TotalRevenue[-nrow(data)]) / data$TotalRevenue[-nrow(data)] * 100)
# data$MoM_Growth <- sprintf("%.2f%%", data$MoM_Growth)
data$MoM_Growth <- round(data$MoM_Growth,2)
return(data)
}
df_MoM_revenue <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
group_by(Month_Year) %>%
summarise(TotalRevenue = sum(TotalAmount))
df_MoM_revenue <- calculate_mom_growth(df_MoM_revenue)
df_MoM_revenue <- df_MoM_revenue %>% filter(!MoM_Growth == "NA%")
plot_MoM_revenue <- df_MoM_revenue %>%
mutate(popup = glue("{Month_Year}
MoM Growth: {MoM_Growth}%")) %>%
ggplot()+
geom_col(data = . %>% filter(MoM_Growth > 0),aes(x = Month_Year,
y = MoM_Growth, text = popup), fill = "darkblue")+
geom_col(data = . %>% filter(MoM_Growth < 0),aes(x = Month_Year, y = MoM_Growth,
text = popup), fill = "darkred")+
labs(title = "MoM Revenue Growth Online Retail",
x = NULL,
y = NULL) +
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(plot_MoM_revenue, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Sebagaimana hasil tren yang didapatkan sebelumnya, pertumbuhan pendapatan terjadi secara berturut-turut di empat bulan terakhir, dan di periode awal cenderung fluktuatif alias naik turun. Adapun pertumbuhan paling tinggi terjadi di bulan september 2011 dan penurunan pendapatan paling signifikan terjadi di bulan April 2011.
plot_monthly_transaction <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
group_by(Month_Year) %>%
summarise(TotalTransaction = length(unique(InvoiceNo))) %>%
mutate(popup = glue("{Month_Year}
Total Transactions: {TotalTransaction}")) %>%
ggplot(aes(x = Month_Year, y = TotalTransaction)) +
geom_point(aes(text = popup), size =3, colour = "darkblue")+
geom_line(linewidth = 1) +
# geom_point(aes(text = popup)) +
geom_point(data = . %>% filter(TotalTransaction== min(TotalTransaction)),
aes(x = Month_Year, y = TotalTransaction),
size = 4.5, colour = "darkred")+
geom_point(data = . %>% filter(TotalTransaction == max(TotalTransaction)),
aes(x = Month_Year, y = TotalTransaction),
size = 4.5, colour = "#eaec42")+
labs(title = "Monthly Total Transactions Online Retail",
x = NULL,
y = NULL) +
theme_minimal() +
theme(plot.title = element_text(size = 15))
ggplotly(plot_monthly_transaction, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Tren total transaksi bulanan yang terjadi di ritel online UK ini juga fluktuatif di periode-periode awal, dan secara stabil terjadi kenaikan di tiga bulan terakhir. Bulan Januari 2011, total transaksi yang terjadi paling sedikit daripada bulan-bulan yang lainnya, sedangkan November 2011 menjadi bulan dengan transaksi yang paling banyak.
calculate_mom_growth <- function(data) {
data$Month_Year <- as.yearmon(data$Month_Year)
data <- data[order(data$Month_Year), ]
data$MoM_Growth <- c(NA, (data$TotalTransaction[-1] - data$TotalTransaction[-nrow(data)]) / data$TotalTransaction[-nrow(data)] * 100)
# data$MoM_Growth <- sprintf("%.2f%%", data$MoM_Growth)
data$MoM_Growth <- round(data$MoM_Growth,2)
return(data)
}
df_MoM_transactions <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
group_by(Month_Year) %>%
summarise(TotalTransaction = length(unique(InvoiceNo)))
df_MoM_transactions<- calculate_mom_growth(df_MoM_transactions)
# df_MoM_transactions <- df_MoM_transactions %>% filter(!MoM_Growth == "NA%")
plot_MoM_trans <- df_MoM_transactions %>%
mutate(popup = glue("{Month_Year}
MoM Growth: {MoM_Growth}%")) %>%
ggplot()+
geom_col(data = . %>% filter(MoM_Growth > 0),aes(x = Month_Year,
y = MoM_Growth, text = popup), fill = "darkblue")+
geom_col(data = . %>% filter(MoM_Growth < 0),aes(x = Month_Year, y = MoM_Growth, text = popup), fill = "darkred")+
labs(title = "MoM Transactions Growth Online Retail",
x = NULL,
y = NULL) +
theme_minimal() +
theme(plot.title = element_text(size = 15))
ggplotly(plot_MoM_trans, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Sebagaimana hasil tren yang didapatkan sebelumnya, pertumbuhan total transaksi terjadi di tiga bulan terakhir, dan di lima bulan awal cenderung fluktuatif. Selain itu, terjadi penurunan secara berturut-turut dari Juni hingga Agustus 2011. Adapun pertumbuhan paling tinggi terjadi di bulan November 2011 dan penurunan pendapatan paling signifikan terjadi di bulan Januari 2011.
plot_monthly_customer <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
group_by(Month_Year) %>%
summarise(TotalCustomer = length(unique(CustomerID))) %>%
mutate(popup = glue("{Month_Year}
Total Customers: {TotalCustomer}")) %>%
ggplot(aes(x = Month_Year, y = TotalCustomer)) +
geom_point(aes(text = popup), size =3, colour = "darkblue")+
geom_line(linewidth = 1) +
# geom_point(aes(text = popup)) +
geom_point(data = . %>% filter(TotalCustomer == min(TotalCustomer)),
aes(x = Month_Year, y = TotalCustomer),
size = 4.5, colour = "darkred")+
geom_point(data = . %>% filter(TotalCustomer == max(TotalCustomer)),
aes(x = Month_Year, y = TotalCustomer),
size = 4.5, colour = "#eaec42")+
labs(title = "Monthly Total Customers Online Retail",
x = NULL,
y = NULL) +
theme_minimal() +
theme(plot.title = element_text(size = 15))
ggplotly(plot_monthly_customer, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Tren total pelanggan bulanan yang melakukan transaksi di ritel online UK ini juga fluktuatif di periode-periode awal, dan secara stabil terjadi kenaikan di tiga bulan terakhir. Bulan Januari 2011, total transaksi yang terjadi paling sedikit daripada bulan-bulan yang lainnya, sedangkan November 2011 menjadi bulan dengan transaksi yang paling banyak.
calculate_mom_growth <- function(data) {
data$Month_Year <- as.yearmon(data$Month_Year)
data <- data[order(data$Month_Year), ]
data$MoM_Growth <- c(NA, (data$TotalCustomer[-1] - data$TotalCustomer[-nrow(data)]) / data$TotalCustomer[-nrow(data)] * 100)
# data$MoM_Growth <- sprintf("%.2f%%", data$MoM_Growth)
data$MoM_Growth <- round(data$MoM_Growth,2)
return(data)
}
df_MoM_Customer <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
group_by(Month_Year) %>%
summarise(TotalCustomer = length(unique(CustomerID)))
df_MoM_Customer<- calculate_mom_growth(df_MoM_Customer)
df_MoM_Customer <- df_MoM_Customer %>% filter(!MoM_Growth == "NA%")
plot_MoM_customer <- df_MoM_Customer %>%
mutate(popup = glue("{Month_Year}
MoM Growth: {MoM_Growth}%")) %>%
ggplot()+
geom_col(data = . %>% filter(MoM_Growth > 0),aes(x = Month_Year,
y = MoM_Growth, text = popup), fill = "darkblue")+
geom_col(data = . %>% filter(MoM_Growth < 0),aes(x = Month_Year, y = MoM_Growth, text = popup), fill = "darkred")+
labs(title = "MoM Customer Growth Online Retail",
x = NULL,
y = NULL) +
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(plot_MoM_customer, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Tren MoM Growth dari total pelanggan bulanan yang melakukan transaksi di ritel online UK ini mirip seperti tren MoM Growth dari total transaksi yang terjadi. Perbedaannya adalah pada pertumbuhan paling tinggi yang terjadi di bulan September 2011.
old_customer_monthly <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
select(Month_Year, CustomerID) %>%
distinct() %>%
select(CustomerID) %>%
duplicated() %>%
which()
plot_monthly_newcustomer <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
select(Month_Year, CustomerID) %>%
distinct() %>%
filter(!row_number() %in% c(old_customer_monthly)) %>%
group_by(Month_Year) %>%
summarise(TotalCustomer = length(unique(CustomerID))) %>%
mutate(popup = glue("{Month_Year}
Total New Customers: {TotalCustomer}")) %>%
ggplot(aes(x = Month_Year, y = TotalCustomer)) +
geom_point(aes(text = popup), size =3, colour = "darkblue")+
geom_line(linewidth = 1) +
# geom_point(aes(text = popup)) +
geom_point(data = . %>% filter(TotalCustomer == min(TotalCustomer)),
aes(x = Month_Year, y = TotalCustomer),
size = 4.5, colour = "darkred")+
geom_point(data = . %>% filter(TotalCustomer == max(TotalCustomer)),
aes(x = Month_Year, y = TotalCustomer),
size = 4.5, colour = "#eaec42")+
labs(title = "Monthly New Customers Online Retail",
x = NULL,
y = NULL) +
theme_minimal() +
theme(plot.title = element_text(size = 15))
ggplotly(plot_monthly_newcustomer, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Kita lihat bersama bahwa hampir selalu terjadi penurunan secara brturut-turut pada jumlah pelanggan baru tiap bulannya. Kenaikan jumlah pelanggan baru hanya terjadi pada bulan Maret, September, dan November 2011. Untuk lebih jelasnya, mari kita coba lihat secara langsung dari MoM Growth.
old_customer_monthly <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
select(Month_Year, CustomerID) %>%
distinct() %>%
select(CustomerID) %>%
duplicated() %>%
which()
calculate_mom_growth <- function(data) {
data$Month_Year <- as.yearmon(data$Month_Year)
data <- data[order(data$Month_Year), ]
data$MoM_Growth <- c(NA, (data$TotalCustomer[-1] - data$TotalCustomer[-nrow(data)]) / data$TotalCustomer[-nrow(data)] * 100)
# data$MoM_Growth <- sprintf("%.2f%%", data$MoM_Growth)
data$MoM_Growth <- round(data$MoM_Growth,2)
return(data)
}
df_MoM_NewCustomer <- df_ready %>% mutate(Month_Year = as.yearmon(InvoiceDate)) %>%
select(Month_Year, CustomerID) %>%
distinct() %>%
filter(!row_number() %in% c(old_customer_monthly)) %>%
group_by(Month_Year) %>%
summarise(TotalCustomer = length(unique(CustomerID)))
df_MoM_NewCustomer<- calculate_mom_growth(df_MoM_NewCustomer)
df_MoM_NewCustomer <- df_MoM_NewCustomer %>% filter(!MoM_Growth == "NA%")
plot_MoM_newcustomer <- df_MoM_NewCustomer %>%
mutate(popup = glue("{Month_Year}
MoM Growth: {MoM_Growth}%")) %>%
ggplot()+
geom_col(data = . %>% filter(MoM_Growth > 0),aes(x = Month_Year,
y = MoM_Growth, text = popup), fill = "darkblue")+
geom_col(data = . %>% filter(MoM_Growth < 0),aes(x = Month_Year, y = MoM_Growth,
text = popup), fill = "darkred")+
labs(title = "MoM New Customer Growth Online Retail",
x = NULL,
y = NULL) +
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(plot_MoM_newcustomer, tooltip="text") %>%
config(displayModeBar = F, scrollzoom = F)
Yups, kita lihat bersama memang hampir selalu terjadi penurunan jumlah pelanggan baru untuk tiap bulannya. Menariknya, meskipun terjadi penurunan jumlah pelanggan baru tiap bulannya, tetapi pendapatan, total transaksi, dan total pelanggan yang melakukan transaksi tetap memiliki tren yang positif. Hal ini bermakna positif di mana banyak pelanggan yang sering melakukan pembelian kembali. Selain itu, fakta ini bermakna bahwa meningkatkan retensi pelanggan adalah salah satu upaya yang perlu dan harus dilakukan. Tujuannya, mejaga pendapatan dari ritel online UK ini tetap meningkat meskipun pada kondisi jumlah pelanggan baru mengalami penurunan seiring berjalannya waktu.
plot_age <- df_ready %>%
mutate(`age customer` = round(AgeCustomer,0)) %>%
ggplot(aes(x = `age customer`)) +
geom_histogram(fill= "#1436e0")+
labs(
title="Customer Age Distribution in Days",
x="Age in Days",
y="Frequency")+
theme_minimal()
ggplotly(plot_age) %>%
layout(showlegend=FALSE) %>%
config(displayModeBar = F)
Pelanggan pada ritel online UK ini banyak yang telah bergabung selama setahun. Lebih dari 50 persen pelanggan ritel online UK telah bergabung selama 6 bulan. Hasil distribusi umur atau lama pelanggan telah bergabung juga sejalan dengan kondisi pertumbuhan pelanggan baru. Distribusi umur pelanggan juga menunjukkan bahwa pelanggan baru yang bergabung jumlahnya sedikit.
df_monthly_habbit <- df_ready %>% select(InvoiceDate, InvoiceNo) %>%
distinct() %>%
mutate(Month = month(InvoiceDate),
Day = day(InvoiceDate)) %>%
group_by(Month,Day) %>%
summarise(Total = n()) %>%
ungroup() %>%
group_by(Day) %>%
summarise(AvgMonthlyTrans = as.integer(median(Total))) %>%
ungroup() %>%
mutate(popup = glue("Date : {Day}
Total Transaction: {AvgMonthlyTrans}"))
df_monthly_habbit %>%
ggplot(aes(x=as.factor(Day), y=AvgMonthlyTrans)) +
geom_bar(stat="identity", aes(fill=AvgMonthlyTrans),show.legend = FALSE)+
labs(title = "Monthly Order Habbit",
x = "Date",
y = NULL)+
theme_minimal()+
theme(axis.title = element_blank(),
legend.position = "none",
plot.title = element_text(hjust = 0.5,size=12, face="bold"),
plot.subtitle = element_text(hjust = 0.5,size=10),
axis.text.y = element_blank(),
axis.text.x=element_text(size=11, face="bold"))+
scale_fill_gradient()+
#my_theme_fill()+
coord_polar() -> polar1
# Daily
df_wday_habbit <- df_ready %>% select(InvoiceDate, InvoiceNo) %>%
distinct() %>%
mutate(Month = month(InvoiceDate),
wday = wday(InvoiceDate)) %>%
group_by(Month,wday) %>%
summarise(Total = n()) %>%
ungroup() %>%
group_by(wday) %>%
summarise(AvgWdayTrans = as.integer(median(Total))) %>%
ungroup() %>%
mutate(popup = glue("wday : {wday}
Total Transaction: {AvgWdayTrans}"))
df_wday_habbit %>%
ggplot(aes(wday,AvgWdayTrans))+
geom_bar(width=1, stat="identity", show.legend = FALSE, aes(fill=AvgWdayTrans))+
labs(
title = "Daily Order Habbit",
x = "Day",
y = NULL)+
scale_x_continuous(breaks = c(1,2,3,4,5,6,7),
labels = c("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"))+
theme_minimal()+
theme(axis.title = element_blank(),
legend.position = "none",
plot.title = element_text(hjust = 0.5,size=12, face="bold"),
plot.subtitle = element_text(hjust = 0.5,size=10),
axis.text.y = element_blank(),
axis.text.x=element_text(size=11, face="bold"))+
scale_fill_gradient()+
coord_polar() -> polar2
#Hourly
df_hourly_habbit <- df_ready %>% select(InvoiceDate, InvoiceNo) %>%
distinct() %>%
mutate(Day = day(InvoiceDate),
Hour = hour(InvoiceDate)) %>%
group_by(Day,Hour) %>%
summarise(Total = n()) %>%
ungroup() %>%
group_by(Hour) %>%
summarise(AvgHourlyTrans = as.integer(median(Total))) %>%
ungroup() %>%
mutate(popup = glue("Hour of Day : {Hour}
Total Transaction: {AvgHourlyTrans}"))
time_range = data_frame(hour = c(0:23))
data_frame(Hour = c(0:23)) %>%
left_join(
df_hourly_habbit ,by= "Hour") %>%
mutate(Hour = as.factor(Hour)) %>%
ggplot(aes(x=Hour,y=AvgHourlyTrans))+
geom_bar(stat="identity",show.legend = FALSE, aes(fill=AvgHourlyTrans))+
labs(title = "Hourly Order Habbit",
x = "Hour of Day",
y = NULL)+
theme_minimal()+
theme(axis.title = element_blank(),
legend.position = "none",
plot.title = element_text(hjust = 0.5,size=12, face="bold"),
plot.subtitle = element_text(hjust = 0.5,size=10),
axis.text.y = element_blank(),
axis.text.x=element_text(size=11, face="bold"))+
scale_fill_gradient()+
coord_polar() -> polar3
grid.arrange(polar1,polar2,polar3, ncol = 3)
Semakin intens warna pada grafik di atas mencerminkan tingkat aktivitas transaksi yang lebih tinggi. Dengan merinci pola waktu ketika pelanggan cenderung berbelanja lebih banyak, kita dapat mengoptimalkan strategi kampanye untuk menyesuaikan dengan kebiasaan pembelian pelanggan. Berikut beberapa periode kebiasan pelanggan melakukan transaksi.
getmodus <- function(v) {
uniqv <- unique(v)
uniqv[which.max(tabulate(match(v, uniqv)))]
}
df_item_different <- df_ready %>%
select(CustomerID,StockCode,Description) %>%
distinct() %>%
group_by(CustomerID) %>%
summarise(item = n()) %>%
ungroup() %>%
mutate( CustomerID = CustomerID,
popup=glue("Customer ID : {CustomerID}
Unique Item : {item}"))
plot_df_item_different <- df_item_different %>%
ggplot(aes(x=CustomerID, y = item)) +
geom_point(aes(color=item, size=item, text=popup), show.legend = FALSE)+
geom_hline(yintercept=getmodus(df_item_different$item),linetype="dashed", color = "black",size=0.5,text=getmodus(df_item_different$item))+
annotate(geom="text",
x=max(df_item_different$CustomerID)-2/10*length(df_item_different$CustomerID),
y=getmodus(df_item_different$item)+500,size=3,
label=paste0("Modus: ",getmodus(df_item_different$item),
", Total: ",round(as.numeric(((tabulate(match(df_item_different$item, unique(df_item_different$item))) %>%
sort(decreasing = T) %>% .[1])/length(df_item_different$CustomerID))*100),2), "% cust"),
color="black")+
labs(title="Unique item that was purchased by each customer",
x="Customer",
y="Total Unique Item")+
theme(
axis.text.x = element_blank()
)+
scale_color_gradient(na.value = "#C0C0C0")+
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(plot_df_item_different, tooltip="text") %>%
layout(showlegend=FALSE) %>%
config(displayModeBar = F)
Ini sangat menarik, hanya 2,14% dari total pelanggan di ritel online UK ini yang hanya pernah membeli satu jenis produk di ritel ini, sisanya membeli lebih dari satu jenis produk. Kondisi ini menjadi pendukung dalam analisis market basket untuk meningkatkan loyalitas pelanggan ke depannya. Selanjutnya, mari kita coba lihat rata-rata basket size dari pelanggan ritel online UK ini.
getmodus <- function(v) {
uniqv <- unique(v)
uniqv[which.max(tabulate(match(v, uniqv)))]
}
df_basket <- df_ready %>%
group_by(CustomerID, InvoiceNo) %>%
summarise(baskets = sum(Quantity)) %>%
ungroup() %>%
group_by(CustomerID) %>%
summarise(
freq = n(),
baskets = median(baskets))%>%
ungroup() %>%
mutate( CustomerID = CustomerID,
popup=glue("customer id : {CustomerID}
Trans. Frequency : {freq}
Avg. basket : {baskets}"))
plot_baskets <- df_basket %>%
ggplot(aes(x=CustomerID,y=baskets)) +
geom_point(aes(color=baskets, size=baskets, text=popup), show.legend = FALSE)+
geom_hline(yintercept=getmodus(df_basket$baskets),linetype="dashed", color = "black",size=0.5,text=getmodus(df_basket$baskets))+
annotate(geom="text",
x=max(df_basket$CustomerID)-2/10*length(df_basket$CustomerID),
y=getmodus(df_basket$baskets)+8000,size=3,
label=paste0("Modus: ",getmodus(df_basket$baskets),
", Total: ",round(as.numeric(((tabulate(match(df_basket$baskets, unique(df_basket$baskets))) %>%
sort(decreasing = T) %>% .[1])/length(df_basket$CustomerID))*100),2), "% cust"),
color="black")+
labs(title="Average baskets for each customer",
x="Customer",
y="Average Baskets")+
theme(
axis.text.x = element_blank()
)+
scale_color_gradient()+
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(plot_baskets, tooltip="text") %>%
layout(showlegend=FALSE) %>%
config(displayModeBar = F)
Ternyata, modus rata-rata basket size pelanggan ialah 72. ini kondisi yang sangat baik, kita bisa melakukan analisis market basket bahkan hingga personalisasi produk rekomendasi bagi pelanggan. Strategi ini dapat meningkatkan kepuasan pelanggan karena pelanggan merasa kebutuhan ataupun keinginannya dipenuhi oleh ritel ini, sehingga meningkatkan kemampuan perusahaan dalam mempertahankan pelanggan.
Negara asal dari ritel online ini menjadi primadona dari negara pelanggan dalam melakukan transaksi, meskipun demikian beberapa negara eropa tentangga juga menjadi negara yang cukup banyak memberikan intensitas dalam bertransaksi. Menariknya adalah negara Australia, di mana negara yang berada di selatan benua asia, cukup memberikan porsi jumlah transaksi dari total transaksi yang terjadi selama satu tahun.
Metode Recency, Frequency, Monetary Value (RFM) merupakan salah satu metode yang sering digunakan untuk segmentasi pelanggan. Metode ini menggunakan pendekatan kebiasaan/perilaku pelanggan dalam bertransaksi melalui riwayat belanja pelanggan. Nilai atau definisi dari parameter metode analisis RFM ini sebagai berikut;
Melalui parameter metode RFM, kita dapat mengetahui karakteristik pelanggan, sehingga memudahkan pelaku usaha dalam memberikan perlakuan yang spesial sesuai karakter pelanggan. Pelanggaan akan dikelompokkan berdasarkan pada kemiripan sifat-sifatnya berdasarkan parameter RFM tersebut.
Terdapat beberapa Bussines Question yang bisa terjawab melalui analisis RFM ini, yakni:
Pada analisis RFM di R, telah tersedia package RFM untuk mempermudah analisis yang dilakukan.
df_rfm <- df_ready %>%
select(InvoiceDate, CustomerID, InvoiceNo, TotalAmount) %>%
distinct() %>%
select(InvoiceDate, CustomerID, TotalAmount)
df_rfm_result <- rfm_table_order(
data = df_rfm,
customer_id = CustomerID,
revenue = TotalAmount,
order_date = InvoiceDate,
analysis_date = as.POSIXct(max(df_ready$InvoiceDate) + 1)
)
df_rfm_result$rfm
Adapun deskripsi kolom, sebagai berikut:
Feature <- c(colnames(df_rfm_result$rfm))
Description <- c("ID unik pelanggan", "Periode terakhir pelanggan melakukan transaksi",
"nilai recency pelanggan", "nilai frequency pelanggan", "nilai monetary pelanggan",
"skor dari recency pelanggan", "skor dari frequency pelanggan",
"skor dari monetary pelanggan", "total skor dari nilai RFM pelanggan")
df_rfm_desc <- data.frame(Feature, Description)
knitr::kable(df_rfm_desc, "pipe")
Feature | Description |
---|---|
customer_id | ID unik pelanggan |
date_most_recent | Periode terakhir pelanggan melakukan transaksi |
recency_days | nilai recency pelanggan |
transaction_count | nilai frequency pelanggan |
amount | nilai monetary pelanggan |
recency_score | skor dari recency pelanggan |
frequency_score | skor dari frequency pelanggan |
monetary_score | skor dari monetary pelanggan |
rfm_score | total skor dari nilai RFM pelanggan |
Pada data frame di atas, terdapat indeks skor dari parameter RFM pelanggan. Secara default, nilai skor tiap parameter pelanggan dari data online retail UK ini didapat dengan teknik quantile dengan ketentuan sebagai berikut:
default_rfm <- as.data.frame(round(quantile(df_rfm_result$rfm$recency_days,
probs = seq(0, 1, 0.20)))) %>%
rename("quantile_recency" = "round(quantile(df_rfm_result$rfm$recency_days, probs = seq(0, 1, 0.2)))") %>%
mutate(recency_score = c(NA,5,4,3,2,1),
quantile_frequency = quantile(df_rfm_result$rfm$transaction_count,probs = seq(0, 1, 0.20)),
frequency_score = c(NA,1,2,3,4,5),
quantile_monetary = quantile(df_rfm_result$rfm$amount,probs = seq(0, 1, 0.20)),
monetary_score = c(NA,1,2,3,4,5)) %>%
drop_na()
default_rfm
Menurut saya, pembagian dari hasil teknik Quantile pada dataset ini sudah cukup baik untuk digunakan scoring parameter RFM. Nilai score sudah merepresentasikan nilai dari RFM pelanggan. Contohnya, pada pelaggan dengan recency 1 sampai 12 hari, memiliki score recency 5 dan sebagainya. Sehingga, saya tidak melakukan manipulasi pembagian quantile secara manual. Akan tetapi, Penentuan scoring bisa disesuaikan dengan kebutuhan bisnis dan dapat menggunakan Unsupervised Machine Learning.
Kemudian, nilai RFM Score didapat dengan rumus sebagai berikut:
RFM Score = recency_score x 100 + frequency_score x 10 + monetary_score
Selanjutnya, mari kita lihat hubungan antar parameter RFM.
scatter_rf <- df_rfm_result$rfm %>%
ggplot(aes(x = transaction_count, y = recency_days), fill = "#1436e0",
color = "#1436e0" ) +
geom_point(aes(text = glue("Recency: {round(recency_days)} days
Frequency: {transaction_count} times")),
fill = "#1436e0",color = "#1436e0" ) +
labs(title = "Recency vs Frequency",
y = "Recency",
x = "Frequency") +
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(scatter_rf, tooltip="text") %>%
layout(showlegend=FALSE) %>%
config(displayModeBar = F)
Dari visualisasi hubungan antara nilai recency dengan frequency, kita lihat bersama bahwa antara dua parameter tersebut tidak terdapat hubungan yang kentara. Hal itu dapat diterima karena nilai kebaharuan pelanggan melakukan transaksi memang tidak dapat merepsentasikan nilai tingkat frekuensi belanja pelanggan tersebut.
Selanjutnya, coba kita lihat pada hubungan nilai recency dengan monetary pelanggan. Mari kita buat dugaan bersama bahwa kedua nilai tersebut tidak memiliki hubungan.
scatter_rm <- df_rfm_result$rfm %>%
ggplot(aes(x = amount, y = recency_days), fill = "#1436e0",
color = "#1436e0") +
geom_point(aes(text = glue("Recency: {round(recency_days)} days
Monetary: £ {amount}")), fill = "#1436e0",
color = "#1436e0") +
labs(title = "Recency vs Monetary",
y = "Recency",
x = "Monetary")+
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(scatter_rm, tooltip="text") %>%
layout(showlegend=FALSE) %>%
config(displayModeBar = F)
Yups, benar sebagaimana yang kita duga bersama. Visualisasi hubungan antara nilai recency dengan monetary melalui scatter plot menunjukkan bahwa antara dua parameter tersebut juga tidak terdapat hubungan yang kentara. Memang benar, nilai yang merepresentasikan periode kapan terkahir kali belanja tidak dapat menunjukkan seberapa besar pelanggan telah membelanjakan uangnya di ritel online ini.
Mari kita lanjutkan pada bagian parameter frequency dengan monetary. Jika kita renungkan, seharusnya terdapat hubungan antara keduanya. Semakin sering seseorang belanja maka semakin banyak juga uang yang telah dibelanjakan dalam suatu tempat belanja tersebut. Sekarang coba kita buktikan renungan kita bersama.
scatter_fm <- df_rfm_result$rfm %>%
ggplot(aes(x = amount, y = transaction_count), fill = "#1436e0",
color = "#1436e0") +
geom_point(aes(text = glue("Frequency: {transaction_count} times
Monetary: £ {amount}")), fill = "#1436e0",
color = "#1436e0") +
labs(title = "Frequency vs Monetary",
y = "Frequency",
x = "Monetary") +
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(scatter_fm, tooltip="text") %>%
layout(showlegend=FALSE) %>%
config(displayModeBar = F)
Yups, antara parameter frequency dengan monetary, terlihat korelasi positif yang cukup kentara.
Okey, untuk memperkuat pemahaman yang kita dapatkan dari scatter plot, mari kita tinjau nilai korelasi antar parameter RFM.
ggcorr(df_rfm_result$rfm %>% select(recency_days,transaction_count,amount),
label = TRUE, label_size = 4, label_color = "white", vjust=1, hjust=0.5)+
labs(
title="Correlation Matrix on RFM Value"
)+
theme_minimal()+
scale_fill_gradient()
Sebagaimana hasil visualisasi scatter, nilai recency bisa dikatakan tidak berkorelasi dengan parameter frequency maupun monetary. Sedangkan parameter frequency cukup berkorelasi positif dengan parameter monetary. Hasil ini memperkuat pemahaman kita dari analisis melalui visualisasi scatter plot sebelumnya.
Penting bagi kita untuk mengetahui distribusi dari tiap parameter RFM. Tentu, tujuannya untuk menghindari hasil segmentasi yang tidak mampu membedakan segmen pelanggan dengan baik. Mari kita coba lihat bersama.
hist_rfm <- rfm_histograms(df_rfm_result, hist_bins = 12, print_plot = F, hist_color = "#1436e0") +
theme_minimal()+
theme(plot.title = element_text(size = 15))
ggplotly(hist_rfm , tooltip = NULL) %>%
config(displayModeBar = F, scrollzoom = F)
Pada parameter recency, data kita memiliki distribusi yang cukup baik meskipun skewed positif dibandingkan dengan persebaran data untuk parameter frequency dan monetary. Data dari dua parameter tersebut sangat tersebar secara skewed positif.
Jika kita lihat distribusi di atas dan meninjau teknik scoring RFM dengan metode quantile, maka kemungkinan besar pelanggan dari ritel online ini mayoritas memiliki score frequency dan monetary yang kecil, sedangkan score recency akan banyak tersebar dinilai yang tinggi.
Sekarang mari kita lakukan segmentasi menggunakan nilai skor dari tiap parameter RFM. Adapun kategori pengelompokan sebagai berikut.
Segment <- c("Champions", "Loyal Customers", "Potential Loyalist",
"New Customers", "Promising", "Need Attention", "About To Sleep",
"At Risk", "Can't Lose Them", "Hibernating", "Lost")
Description <- c("Bought recently, buy often and spend the most",
"Spend good money. Responsive to promotions",
"Recent customers, spent good amount, bought more than once",
"Bought more recently, but not often",
"Recent shoppers, but haven’t spent much",
"Above average recency, frequency & monetary values",
"Below average recency, frequency & monetary values",
"Spent big money, purchased often but long time ago",
"Made big purchases and often, but long time ago",
"Low spenders, low frequency, purchased long time ago",
"Lowest recency, frequency & monetary scores")
Recency <- c("4-5","2-5","3-5","4-5","3-4","2-3","2-3","1-2","1","2-3","1-2")
Frequency <- c("4-5","3-5","1-3","1","1","2-3","1-2","2-5","4-5","2-3","1-2")
Monetary <- c("4-5","3-5","1-3","1","1","2-3","1-2","2-5","4-5","2-3","1-2")
rule_segment <- data.frame(Segment, Description)
knitr::kable(rule_segment, "pipe")
Segment | Description |
---|---|
Champions | Bought recently, buy often and spend the most |
Loyal Customers | Spend good money. Responsive to promotions |
Potential Loyalist | Recent customers, spent good amount, bought more than once |
New Customers | Bought more recently, but not often |
Promising | Recent shoppers, but haven’t spent much |
Need Attention | Above average recency, frequency & monetary values |
About To Sleep | Below average recency, frequency & monetary values |
At Risk | Spent big money, purchased often but long time ago |
Can’t Lose Them | Made big purchases and often, but long time ago |
Hibernating | Low spenders, low frequency, purchased long time ago |
Lost | Lowest recency, frequency & monetary scores |
# We label the various segments
segment_names <- c("Champions", "Loyal Customers", "Potential Loyalist",
"New Customers", "Promising", "Need Attention", "About To Sleep",
"At Risk", "Can't Lose Them", "Hibernating", "Lost")
# We set the upper and lower bounds for recency, frequency, and monetary for the above segments
recency_lower <- c(4, 2, 3, 4, 3, 2, 2, 1, 1, 2, 1)
recency_upper <- c(5, 5, 5, 5, 4, 3, 3, 2, 1, 3, 2)
frequency_lower <- c(4, 3, 1, 1, 1, 2, 1, 2, 4, 2, 1)
frequency_upper <- c(5, 5, 3, 1, 1, 3, 2, 5, 5, 3, 2)
monetary_lower <- c(4, 3, 1, 1, 1, 2, 1, 2, 4, 2, 1)
monetary_upper <- c(5, 5, 3, 1, 1, 3, 2, 5, 5, 3, 2)
# We use the segments and the bounds we previously established to group our users into different segments
segment <- rfm_segment(df_rfm_result,
segment_names,
recency_lower,
recency_upper,
frequency_lower,
frequency_upper,
monetary_lower,
monetary_upper)
segment %>% group_by(segment) %>% summarise(total_customer = n())
Dapat kita lihat bersama, bahwasannya data histori belanja pelanggan ritel online ini hanya memiliki 8 segmen dengan segmen Others
merupakan kategori pelanggan yang tidak memenuhi semua kategori yang telah didefinisikan. Adapun anggota segmen terbanyak ialah Loyal Customer
. Sedangkan banyaknya pelanggan yang terdeteksi churn atau Lost
sejumlah 389.
Selanjutnya, mari kita coba lihat karakter RFM dari tiap segmen pelanggan.
segment <- segment %>%
mutate(
segment = factor(segment, levels = c("Others", "Lost", "At Risk", "About To Sleep", "Need Attention", "Potential Loyalist", "Loyal Customers", "Champions"))) %>%
select(customer_id, segment, rfm_score, transaction_count, recency_days, amount)
df_profile <- segment %>%
group_by(segment) %>%
summarise(
total_customer = n(),
total_monetary = sum(amount),
total_frequency = sum(transaction_count),
min_frequency = min(transaction_count),
max_frequency = max(transaction_count),
avg_frequency = as.integer(median(transaction_count)),
min_monetary = min(amount),
max_monetary = max(amount),
avg_monetary = median(amount),
min_recency = min(recency_days),
max_recency = max(recency_days),
avg_recency = median(recency_days)
) %>%
mutate(
persen_customer = round(total_customer/sum(total_customer)*100,2)
)
df_profile %>%
mutate(popup = glue("Segment: {segment}
Total Customer: {total_customer}
Med. Recency: {round(avg_recency)}
Med. Frequency: {avg_frequency}
Med. Monetary: £ {comma(avg_monetary)}"),
popup = str_replace_all(popup, pattern = "\n", replacement = "<br>")) %>%
hchart(
"treemap",
hcaes(x = segment,
value = persen_customer,
color = persen_customer,
text = popup),
dataLabels = list(enabled = TRUE, format='{point.segment}<br/>{point.persen_customer}%<br/>'),
tooltip = list(pointFormat = "{point.popup}")
) %>%
hc_title(text = "RFM Segmentation Result by RFM Score: Customer Distribution") %>%
# hc_colorAxis(stops = color_stops(colors = my_color)) %>%
hc_legend(enabled = F)
Kondisi ritel online UK ini dalam keadaan yang cukup baik. Hampir 65% pelanggan tersebar dalam kategori pelanggan yang sering berbelanja, loyal dalam mengeluarkan uang dengan jumlah yang besar, dan responsif terhadap promo. Pelanggan-pelanggan tersebut masuk dalam segmen Champions, Loyal Customers, dan Potential Loyalist. Meskipun demikian terdapat 9% dari total pelanggan, tepatnya 389 pelanggan tergolong telah berhenti berbelanja (Lost) dan 5% pelanggan beresiko berhenti belanja (About to Sleep). Lebih dari itu, cukup disayangkan teradapat 10% pelanggan yang suka menghabiskan uang dalam jumlah besar, akan tetapi sudah lama tidak berbelanja dan beresiko churn (At Risk). Oleh karena itu, ritel online UK ini harus mengatasi sekitar 40% pelanggan yang dalam kondisi kurang baik dalam tanda kutip menguntungkan perusahaan, sembari tetap menjaga pelanggan-pelanggan yang sangat menguntungkan.
Adanya analisis perbedaan karakter dari berbagai segmen pelanggan ini dapat menjadi referensi untuk digunakan penentuan strategi yang diterapkan, istilahnya personalisasi kampanye pemasaran. Strategi pemasaran yang disesuaikan dengan karakter suatu segmen dirasa dapat meningkatkan gairah belanja pelanggan. Sebagaimana, diri kita ketika diberi makanan yang kita suka. Bayangkan saja ketika kita diberi makanan yang kita tidak suka atau bahkan alergi, maka kita akan menolaknya. Dengan demikian, mari kita tinjau lebih mendetail terkait profil segmen pelanggan dari ritel online UK ini.
plot_profile_recency <- df_profile %>% mutate(
popup = glue("Total Customer: {total_customer} ({round((total_customer/sum(total_customer))*100,1)})%
Med. Recency: {round(avg_recency)} days")
) %>%
ggplot(aes(y = segment, x = avg_recency)) +
geom_bar(aes(fill=segment, text=popup),stat="identity", show.legend = FALSE)+
geom_text(aes(label = paste0('Med: ',round(avg_recency,0)),
x = avg_recency + 20),
size = 3,
color = "black")+
#geom_text(aes(label=paste0(round((total_customer/sum(total_customer))*100,1),"% cust"),
# y=total_monetary+220000),size=3, color="black")+
labs(
title="Profiling: Median Recency per Cluster",
x = "Median Recency",
y= NULL) +
scale_x_continuous(limits = c(0, 310))+
scale_fill_ordinal() +
theme_minimal()+
theme(plot.title = element_text(size = 15),
legend.position = "none")
ggplotly(plot_profile_recency, tooltip = "text") %>%
config(displayModeBar = F, scrollzoom = F)
plot_profile_freq <- df_profile %>% mutate(
popup = glue("Total Customer: {total_customer} ({round((total_customer/sum(total_customer))*100,1)})%
Total Frequency: {total_frequency} ({round((total_frequency/sum(total_frequency))*100,1)})%
Med. Frequency: {avg_frequency} Order"),
perc_freq = ({round((total_customer/sum(total_customer))*100,1)})
) %>%
ggplot(aes(y = segment, x = avg_frequency)) +
geom_bar(aes(fill=segment, text=popup), stat="identity", show.legend = FALSE)+
geom_text(aes(label = paste0(round((total_frequency/sum(total_frequency))*100,1), "%"), x = avg_frequency + 6),size=3, color="black")+
#geom_text(aes(label=paste0(round((total_customer/sum(total_customer))*100,1),"% cust"),
# y=total_monetary+220000),size=3, color="black")+
labs(
title="Profiling: Median Frequency per Cluster",
x = "Median Frequency",
y= NULL) +
# scale_x_continuous(limits = c(0, 310))+
scale_fill_ordinal() +
theme_minimal()+
theme(plot.title = element_text(size = 15),
legend.position = "none")
ggplotly(plot_profile_freq, tooltip = "text") %>%
config(displayModeBar = F, scrollzoom = F)
Wow, hampir 60% dari semua transaksi yang terjadi di ritel online UK ini berasal dari pelanggan segmen Champions. Kemudian, 25% berasal dari Loyal Customers. Alangkah baiknya ritel online ini menerapkan strategi yang mampu menjaga loyalitas pelanggan segmen tersebut, seperti pelayan khusus untuk dapat memporelah produk-produk limited edition, dan sebagainya.
Kemudian, pada segmen-segmen yang telah berhenti belanja dan telah lama tidak melakukan belanja, mereka dapat diberikan promo khusus pada barang-barang yang mungkin mereka sukai. Untuk mengetahui produk yang pelanggan sukai, maka dapat dilakukan analisis dengan metode Collaborative Filtering.
plot_profile_mon <- df_profile %>% mutate(
popup = glue("Total Customer: {total_customer} ({round((total_customer/sum(total_customer))*100,1)})%
Total Monetary: £ {comma(total_monetary)} ({round((total_monetary/sum(total_monetary))*100,1)})%
Med. Monetary: £ {comma(avg_monetary)}"),
perc_mon = round((total_monetary/sum(total_monetary))*100,1)
) %>%
ggplot(aes(y = segment, x = avg_monetary)) +
geom_bar(aes(fill= segment, text = popup), stat="identity", show.legend = FALSE)+
geom_text(aes(label=paste0(perc_mon,"%"),x =avg_monetary + 120),size=3, color="black")+
labs(
title="Profiling: Monetary per Cluster",
x = "Median Monetary",
y = NULL) +
scale_x_continuous(limits = c(0, 2000))+
scale_fill_ordinal() +
theme_minimal()+
theme(plot.title = element_text(size = 15),
legend.position = "none")
ggplotly(plot_profile_mon, tooltip = "text") %>%
config(displayModeBar = F, scrollzoom = F)
Sejalan dengan persentase total frequency dari semua total transaksi yang terjadi, persentase total monetary dari segmen champions dan Loyal Customers mampu memberikan lebih dari 80% pendapatan yang diperoleh ritel online ini.
Berikut adalah beberapa strategi yang dapat diterapkan untuk setiap kelompok pelanggan:
Champions:
Loyal Customers:
Potential Loyalist:
Need Attention:
About To Sleep:
** Buat kampanye pemasaran khusus untuk membangunkan minat. ** Tawarkan diskon atau promosi eksklusif untuk mendorong pembelian. ** Kirim pengingat tentang produk atau layanan yang mereka sukai.
At Risk:
Lost:
Melalui penerapan strategi ini, ritel online UK dapat lebih efektif mengelola dan meningkatkan hubungan dengan setiap kelompok pelanggan sesuai dengan kebutuhan dan karakteristik mereka.
Note
Manajemen keuangan yang baik sangat penting untuk memastikan bahwa pemberian insentif atau diskon tidak merugikan profitabilitas ritel online UK. Berikut beberapa referensi cara untuk mengelola keuangan dan tetap untung sambil memberikan insentif kepada pelanggan:
Terdapat serangkaian metode analisis yang dapat dikembangkan dan dilakukan untuk semakin mengembangkan ritel online UK ini:
Market basket Analysis: Melalu analisis ini, ritel online UK dapat menyediakan bundling package dengan promo tertentu untuk meningkatkan pembelian dan melakukan cross selling.
Personalized Product Recommendations based on Collaborative Filtering: Strategi bisnis sebelumnya, seperti insentif dan promo alangkah baiknya diberikan terhadap produk yang benar-benar diinginkan atau disukai oleh pelanggan. Hal itu dapat dicapai dengan melakukan metode analisis Collaborative Filtering.
Time Series Analysis: Meneliti pola dan tren dalam data transaksi sepanjang waktu. Ini membantu dalam memahami bagaimana perilaku pembelian berubah seiring waktu, misalnya, apakah ada peningkatan penjualan pada musim tertentu.