Datamining för handeln 2 – Klustring av kunder i R

Ämne: Avancerad Analys

Inledning

En annan välkänd tillämpning av datamining-metoder hos företag i handelssektorn är cluster analysis på kunddata. De flesta företag som på ett eller annat sätt särskiljer sina kunder (näthandel, postkatalog, …) har ett kundregister. En smart gruppering av kunder – som dessutom görs om regelbundet – kan bli första steget mot skarpare och mer välriktade reklamkampanjer. Kundklustring kan också ses som förarbete för djupare undersökning av kundbeteendet.

I detta inlägg visar jag ett möjligt sätt att göra en sådan enkel kundgruppering. Som i förra inlägg används som verktyg.

Varning

Det kan inte upprepas nog: alla datamining-projekt bör vara inkrementella och i nära dialog med verksamheten. Den europeiska standarden CRISP-DM för datamining formulerades just  för ått spegla detta:

CRISP-dm

Med en sådan projektideologi i åtanke kan kundklustring ses som en första fas i en omfattande studie av företagets säljflöden, kundernas lojalitet, mönster i köpvanor etc.

Transaktionsdata

Precis som i förra inlägget använder vi säljdata från ett fiktivt företag, Foodmart. En MySQL dump av transaktionsdatabasen kan hämtas här.

I vårt exempel här ska vi gruppera Foodmarts kunder enbart på deras köpbeteende, för enkelhetens skull. Läsaren kan se en sådan gruppering som en avancerad variant på RFM-analys. Kvalitativa attribut som kön, yrke, familjestatus eller bilägande betraktas inte. Det finns annars ett antal klassiska metoder att inkludera sådana attribut, och jag hänvisar den intresserade läsaren dit.

De attributen som jag har valt för att studera kundbeteende är:

  •  max (kvittobelopp)
  •  min (kvittobelopp)
  •  antal kundbesök
  •  medelväde på antal olika produkttyper per kvitto
  •  daginkomst = årsinkomst/365

De första tre mätetal beräknas direkt från sales, och den fjärde är från kunddimensionen. Vi samplar kunddata och begränsar uttaget till 200 kunder. Motsvarande SQL-fråga som ger datat är

SELECT X.CUSTOMER_ID,  
       MAX(SALES) AS MAX_SALES, 
       MIN(SALES) AS MIN_SALES, 
       COUNT(TIME_ID) AS NBR_SHOPPING_TOURS,
       AVG(VARIETY_INDICATOR) AS CAT_VARIETY_INDICATOR,
(REPLACE(SUBSTRING(YEARLY_INCOME,2,3),'K','')*1000/365) AS INCOME_PER_DAY
FROM
(SELECT T.TIME_ID, 
        CUSTOMER_ID , 
        PRODUCT_CATEGORY,
        SUM(STORE_SALES) AS SALES,
        COUNT(DISTINCT PRODUCT_CATEGORY) AS VARIETY_INDICATOR
 FROM SALES_FACT_1997 
   JOIN PRODUCT ON SALES_FACT_1997.PRODUCT_ID=PRODUCT.PRODUCT_ID
   JOIN PRODUCT_CLASS ON PRODUCT_CLASS.PRODUCT_CLASS_ID = PRODUCT.PRODUCT_CLASS_ID
   JOIN TIME_BY_DAY T ON T.TIME_ID = SALES_FACT_1997.TIME_ID
   GROUP BY TIME_ID, CUSTOMER_ID
) X
JOIN CUSTOMER ON CUSTOMER.CUSTOMER_ID =X.CUSTOMER_ID
GROUP BY X.CUSTOMER_ID
LIMIT 200

Analysen

Precis som förut, exporterar vi raderna som en csv-fil och importerar den i . För att kunna köra vår analys använder vi följande R-paket:

  • cluster, fpc, vegan och labdsv  för själva klustringen
  • RandomForest för att generera en användbar modell för klassificering av de övriga kunderna (här tolkar man de ursprungliga 200 som training set)
  • rpart för att illustera ett möjligt beslutsträd för klassificeringen
  • RColorBrewer för fina färger i bilderna.

Först standardiserar vi vårt data. Detta är ett nödvändigt steg för att klustringsalgoritmerna, som väsentligen grupperar punkter i  det 5-dimensionella rummet med avseende på hur ”nära” de är till varandra – och då vill man, så att säga, skala om, så att allt blir jämnt.

Nästa steg är att testköra klustringsalgoritmen flera gånger med olika förvalt antal klustrar (2 till 10). Ett numeriskt kriterium, SSI änvänds sedan för att bestämma det bästa värdet för antal klustrar.

mydata<-read.csv("cust_num200.csv")  #läs in kunddata, minst 200 kunder

clust <- mydata[,2:6] #ta bort kund_id, stör beräkningarna

library("vegan")
library("labdsv")
library("cluster")
library("fpc") #ladda klustringsbibliotek

standard <- decostand(clust, "normalize") #normalisera
cascade <- cascadeKM(standard, inf.gr=2, sup.gr=10, iter=100, criterion="ssi") #prova olika antal klustrar 2 till 10
plot(cascade, sortg=TRUE) #visa bild  

Vi ser att det bästa SSI-värdet uppnås när vi väljer att gruppera datat i 10 klustrar. Detta väljs nu som ”best fit”.

fit<-kmeans(standard,10,nstart=10000) #kör algoritmen för 10 klustrar
clusplot(mydata, fit$cluster, color=TRUE, shade=TRUE,labels=2, lines=0) #rita

plotcluster(mydata, fit$cluster) #rita
På bilden ser man projektionen av våran 5-dimensionella mängd av datapunkter på de 2 dimensionerna som bidrar mest till det blir olika grupper i datat.  Man kan dock se att vissa klustrar är bättre separerade från de andra på nästa bild:

Här är klustrar 4 och 1 nästan perfekt separerade. men det syns att algoritmen har inte gjort ett särskilt bra jobb i att separera t.ex. kluster 6 och 3.

Lägg på de hittade klustrar till urspringsdatat:

#append the found cluster identificators
mydata$clusters <- as.factor(fit$cluster)
mydata
kund_id  max_sales    min_sales antal_köpturer  variety_ind  income_per_day clusters
8207       48.24        11.33      5             4.2000      191.78082        4
6775       51.53        18.02      4             3.2500       82.19178        7
7382       33.38         1.18      3             4.0000      301.36986        1
4616       11.87        11.87      1             2.0000       82.19178        9
6554       34.04         3.14      9             3.4444      191.78082        1
8310       45.11        15.58      6             3.8333       82.19178        7
575        15.54        15.54      1             2.0000      136.98630        1
1895       19.05        19.05      1             4.0000      136.98630        9

Nu är vi redo att bygga scoring model för de resterande (och eventuella nya) kunder.

library("randomForest")
library("RColorBrewer")
#importera klassifikationsbibliotek och grafik
rf<-randomForest(clusters ~ ., data=mydata, importance=TRUE,proximity=TRUE)
#bygg modellen
print(rf) #undersök hur bra modellen är MDSplot(rf,mydata$clusters) #rita

Resultatet blir att på binära beslut och 500 slumpmässiga beslutsträd får vi 13% felklassificerade kunder i training set, vilket normalt sett är helt acceptabelt. Modellen (rf) kan exporteras i XML-format och användas i ett skoring-verktyg i produktionsmiljön.

Confusion matrix:
    1 2  3  4  5  6  7 8  9 10 class.error
1  34 0  0  3  0  0  0 0  2  0  0.12820513
2   0 9  0  0  1  0  0 0  0  0  0.10000000
3   0 0 14  0  0  0  0 1  0  0  0.06666667
4   0 0  1 26  0  0  0 0  2  0  0.10344828
5   0 2  0  0 10  0  0 0  0  0  0.16666667
6   1 0  0  0  0 15  1 0  5  0  0.31818182
7   0 0  0  0  0  0 22 0  0  0  0.00000000
8   0 0  1  0  0  0  1 4  0  0  0.33333333
9   3 0  0  0  0  1  0 0 31  0  0.11428571
10  0 0  0  0  1  0  0 0  0  9  0.10000000

Slutligen kan vi titta på ett möjligt beslutsträd för att få ett hum om vad som försiggår i RandomForest-modellen:

library("rpart")  #importera beslutsträd-modulen
treefit <- rpart(clusters ~ max_sales+min_sales+nbr_shopping_tours+cat_variety_indicator+income_per_day,method="class", data=mydata) #bygg ett träd

plot(treefit,uniform=TRUE, main="Classification Tree Clusters")
text(treefit,use.n=TRUE, all=TRUE, cex=.8 ) #plotta trädet

Klustringsprocessen (och skoringen av obehandlade kunder) kan Bizone AB hjälpa er att automatisera. schemalägga och integrera med befintliga BI-flöden med till exempel PDI Kettle eller  Integration Services .