Python’da RFM Analizi ve KMeans Algoritmasıyla Müşteri Segmentasyonu

Emre Yüksel
KaVe

--

Merhaba, bu yazımızda sizlerle RFM analizinin ne olduğunu, nasıl yorumlandığını, örnek bir veride Python kullanarak nasıl uygulandığını ve Kmeans algoritmasını da kullanarak müşteri segmentasyonun nasıl yapıldığından bahsedeceğim.

RFM analizi müşterinin geçmiş satın alım davranışlarını inceleyip, onların yenilik, sıklık ve parasallık özelliklerini hesaplayarak müşterileri bölütlemeye (segmente etmeye) yarayan bir analiz tekniğidir.

Recency — Yenilik (R) — Son satım alımdan bugüne kadar geçen süre
Frequency — Sıklık (F) — Toplam satın alım sayısı
Monetary — Parasallık (M) — Tüm satın alımların parasal toplamı

Her müşteriye ayrı ayrı yenilik(R), sıklık(F) ve parasallık(M) skoru atanır. Bu skor 1'den 5'e kadar skor alır. Biz yapacağımız örnekte müşterileri 1–4 arası skorlayacağız. 1 en iyi, 4 en kötü olarak tanımlanabilir. Yani her skor için 4 ihtimalimiz olduğundan, 4x4x4 = 64 farklı kombinasyon oluşturabiliriz. Böylece detaylı bir bilgi edinmek için yeterli kombinasyon elde etmiş oluruz.

RFM Analizinin yorumlanması[1]

Elde ettiğimiz yenilik(R), sıklık(F), parasallık(M) skorlarını yan yana tek bir skor olarak birleştirip RFM skorumuzu elde ederiz. RFM skorlarının genel yorumlanması yukarıdaki tablodaki gibidir. İşletmelerin türüne göre final RFM skoruna ulaşırken R,F ve M skorlarında göreceli olarak anlamını arttırabilir veya azaltabilirsiniz. Örneğin, sizin işletmeniz beyaz eşya satan bir işletmeyse müşterilerinizin yüksek fiyatlı alışverişler yapmanızı beklersiniz fakat satın alım sıklığı ve yenilik zamanı düşük olacaktır. Bu yüzden bu şartlar altında ayrı ayrı skorları yorumlamak daha doğru sonuçlar verir.

Şimdi RFM analizini Python kullanarak örnek veri üzerinde uygulamasını yapalım. Veriyi inceleyip, indirip, uygulamalı olarak kendiniz denemek isterseniz buraya tıklayarak ulaşabilirsiniz.

RFM analizinin Python uygulaması

import pandas as pd
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# Verimizi okuyoruz
data = pd.read_csv('sales_data_sample.csv',encoding =
'unicode_escape')

Gerekli kütüphaneleri yükleyip, verimizi okuduktan sonra verimizin boyutuna, büyüklüğüne, eksik değerlerine ve gerekli analizleri yapıyoruz. RFM analizinin nasıl yapıldığına odaklanmak istediğimden verinin analiz kısmının kodlarını paylaşarak yazıyı karmaşıklaştırmak istemiyorum. Ama gerçek bir veriyle uğraşırken verileri uzunca ve detaylı bir şekilde analizden geçirmeniz gerekeceğini unutmayın. Bu örnek verimiz de RFM analizi için kullanacağımız öznitelikler:

CUSTOMERNAME = Müşterilerimizin(burda bizim için şirketler) isimleri

ORDERDATE= Ürünün satın alım tarihi (Recency için kullanılacak)

ORDERNUMBER= Satışın numarası(kimliği) (Frequency için kullanılacak)

SALES= Satışın maddi miktarı (Monetary için kullanılacak)

STATUS= Yapılan satışın son durumu

data['STATUS'].unique()Output: array(['Shipped', 'Disputed', 'In Process', 'Cancelled', 'On Hold','Resolved'], dtype=object)

STATUS sütununda yapılan satışların çeşidi verilmektedir. Aralarında işlemi devam eden, beklemede olan ve iptal edilen satışları almayacağız. Kontrol amaçlı satış yapılmış ürünler arasında fiyatı negatif girilen değer var mı bakmalıyız. Çünkü parasallık değişkenini oluştururken tüm satışların fiyatını toplayacağız. Negatif değer içermesi sonucumuzu yanıltacaktır.

data[(data['STATUS']=='Shipped') & (data['SALES']<0)].shape[0]Output: 0

İptal edilen siparişlerin satış değerinin 0 dan küçük değerlerinin sayısına baktığımızda 0 olduğunu görüyoruz. Yani STATUS == ‘’Shipped’’ olan satışları herhangi bir problemle karşılaşmadan alabiliriz..

RFM Skorları için özniteliklerin hazırlanması

rfm_data['ORDERDATE']=pd.to_datetime(rfm_data['ORDERDATE'])
rfm_data['ORDERDATE'].max()
Output: Timestamp('2005-05-17 00:00:00')NOW=dt.datetime(2005,5,18) # Bir gün sonrasını aldık

Yapılan siparişlerin tarih olarak en yenisini bulduktan sonra Recency(yenilik) değişkenini hesaplamak için baz alınan günü en yeni tarihten 1 gün sonrasına atadık. Veri setindeki en yeni satış 17 Mayıs 2005 tarihinde yapıldığı için 18 Mayıs 2005 bizim baz günümüz oldu. Eğer ki sürekli güncellenen canlı akış verisi olsaydı bu değer bulunduğunuz günün tarihi alınabilirdi.

RFM_FINAL = rfm_data.groupby('CUSTOMERNAME').agg({
'ORDERDATE': lambda x: (NOW - x.max()).days,
'ORDERNUMBER': lambda x: len(x.unique()),
'SALES':'sum'})
RFM_FINAL.rename(columns={'ORDERDATE': 'recency',
'ORDERNUMBER': 'frequency',
'SALES': 'monetary'}, inplace=True)

RFM analizi için müşteri isimlerini index alarak her bir sipariş için satın alım tarihinden baz aldığımız tarihi arasındaki gün farkını, her şirket için satın alım sayısını ve bu satın alımların maddi açıdan değerlerinin toplamını bulup bir DataFrame oluşturduk.

DataFrame’nin çıktısı bu şekildedir.

quantiles = RFM_FINAL.quantile(q=[0.25,0.5,0.75])def R_Class(x,p,d):
if x <= d[p][0.25]:
return 4
elif x <= d[p][0.50]:
return 3
elif x <= d[p][0.75]:
return 2
else:
return 1

# Arguments (x = value, p = recency, monetary_value, frequency, k = quartiles dict)
def FM_Class(x,p,d):
if x <= d[p][0.25]:
return 1
elif x <= d[p][0.50]:
return 2
elif x <= d[p][0.75]:
return 3
else:
return 4

RFM için oluşturduğumuz tablonun quantile değerlerini belirleyerek bir değişkene atadık ve bu değişkenimizi R,F ve M skorlarını atayabileceğimiz fonksiyonumuzda eşik değerleri olarak kullandık. Örneğin toplam satış miktarı küçükten büyüğe sıralandıktan sonra herhangi bir müşterinin toplam satış değeri 0.25 quantile değerinden küçük ise o müşterinin monetary(parasallık) skoruna 4 atarız.

RFM_FINAL['R_score'] = RFM_FINAL['recency'].apply(R_Class,args=('recency',quantiles))
RFM_FINAL['F_score'] = RFM_FINAL['frequency'].apply(R_Class,args=('frequency',quantiles))
RFM_FINAL['M_score'] = RFM_FINAL['monetary'].apply(R_Class,args=('monetary',quantiles))
RFM_FINAL['RFM_Score'] = RFM_FINAL['R_score'].map(str) + RFM_FINAL['F_score'].map(str) + RFM_FINAL['M_score'].map(str)

Oluşturduğumuz fonksiyonu DataFrame’deki sütunlara uyguladıktan sonra ayrı ayrı R, F, M skorlarımızı bulmuş olduk. Bunların her birini kendi ismindeki değişkene kaydettik. Final RFM skoruna ulaşmak içinse bulduğumuz skorları string haline çevirerek yanyana getirdik ve RFM_SCORE değişkenine kaydettik.

Ve elimizde her müşterinin RFM skoru edilmiş haline ulaştık. Bu sayede her müşterimizi segmente etmiş olduk ve bu skorları kullanarak hem müşteri davranışlarına yorum getirebilir hem de gelecek için bir strateji belirleyebiliriz.

RFM skorları belirlenmiş bu müşterilerimizin kendine has davranış özellikleri olduğundan bunlarıda kendi içlerine kümeleyebilir. Yazının devamında bunu yapmak için K merkezli öbekleme (KMeans) algoritmasını görücez.

KMeans Algoritması ve Müşterilerimizin kümelenmesi

K merkezli öbekleme (KMeans), iteratif bir algoritmadır. Önceden herhangi bir etiketlenmiş veri verilmediğinden gözetimsiz öğrenme algoritmasıdır. Elinizdeki veri setini birbiriyle kesişmeyecek şekilde K adet öbeğe bölmektedir. Veri setindeki her nokta bu öbeklerden birine ait olur. Veri noktalarını öbeklere mesafesinin karesinin toplamını minimum yapacak şekilde atanır.

Adımları sıralarsak;

1- Öbek sayınızı (K) seçtikten sonra rastgele K adet merkez seçilir.

2- Her veri noktasıyla merkez arasındaki uzaklık hesaplandıktan sonra en yakın öbeğe atanır.

3- Daha sonra öbeklerde bulunan verilerin ortalamasına göre yeni merkezler belirlenir ve tekrardan noktalar yakın olduğu öbeklere atanır.

4- Bu işlem öbek merkezlerinde değişiklik olmayana kadar devam eder.

Dikkat: 1. adımda K adet merkezin rastgele atandığını söylemiştik. KMeans iteratif bir algoritma olduğu için farklı atanacak merkezler farklı kümelere sebep olmaktadır çünkü algoritmamız local optimuma ulaşırsa orada kalıcak ve global optimuma yakınsayamayacaktır. Bu yüzden algoritmayı merkezleri farklı atayarak birkaç kez çalıştırıp mesafe kareler toplamını sonuç olarak en küçük döndüren sonucu kullanmak başarınızı arttıracaktır.

Algoritmanın amaç fonksiyonuna baktığımızda;

K öbekleme için amaç(objective) fonksiyonu

xi’ler girdileri, μk öbek merkezlerini göstermektedir.

Eğer μk merkezi, xi girdisine uzaklık olarak en yakınsa girdiden o öbek sorumludur. Bunu göstermek için w 1 değerini, değilse 0 değerini almaktadır. Yani bütün öbek merkezleri veri girdisi için yarışmakta ve en yakın olduğu için biri o nokta için öbek olarak seçilmektedir.

Uygulamanın devamı

Algoritmamız veriler arasındaki benzerliği mesafe bazlı ölçtüğü için her değişken aynı birimde olmayabileceğinden standartlaştırma aşaması önemli(örneğin bir evin oda sayısı 3 iken fiyatı 150000 gibi bir rakam olabilir). Standardize etmemiz verimizde eşit varyans elde etmemizi sağlar. Böylece noktalarımızı eşit olarak ağırlandırabiliriz. Amaç fonksiyonuna dikkat ederseniz algoritmamız her küme için varyans tahmini yapmakta ve bunu minimize etmeye çalışmaktadır. Çünkü öbek içinde elde edilen verilen birbirine yakın yani benzer olmasını isteriz.

Dağılıma göre çarpıklık(Skewness) karşılaştırması
rfm_vis = RFM_FINAL[['recency','frequency','monetary']]plt.figure(figsize=(10,6))sns.distplot(rfm_vis['recency'],label='recency')plt.title('Distribution of Recency')
plt.legend()
plt.show()
Logaritma dönüşümünden önce recency dağılımı

Verilen veriyi incelediğimizde standartlaştırmadan önce logaritma dönüşümü yapacağız. Bunu yapmamızın sebebi recency,frequency,monetary değerlerine baktığımızda çarpıklık(skewness) söz konusu olması. Yukarıdaki ilk fotoğrafta dağılıma göre çarpıklık çeşitlerini, ikinci fotoğrafta logaritma dönüşümü uygulanmadan önceki recency dağılımını görüyorsunuz.(Koda benzer olarak isterseniz diğer değişkenlerin dağılımınıda inceleyebilirsiniz). Bizim dağılımımızda negatif çarpıklık söz konusu. Algoritmamızda uzaklık metriğimizi öklid kullandığımız için bu çarpıklık sonucumuzu kötü etkileyebilir. Bu yüzden önce logaritma dönüşümü yaparak çarpıklaştırmayı bir miktar düzeltip daha sonra standartlaştırma işlemi uygulayacağız.

rfm_vis = np.log(rfm_vis)

Logaritma dönüşümü sonrası dağılımlar aşağıdaki gibidir.

Logaritma dönüşümü sonrası R,F,M dağılımları

Standartlaştırma işlemimizide aşağıdaki kodla uyguluyoruz.

scaler = StandardScaler()scaler.fit(rfm_vis)
data_normalized = scaler.transform(kmd_log)

Daha sonra K merkezli öbekleme metodu için öbek sayımızı seçmek için dirsek yöntemini kullanıcaz. Dirsek metodu, x ekseninde öbek sayısı, y ekseninde hatanın kareler toplamını grafik haline bakarak kırılımın azaldığı noktaları tespit etmemizi sağlar. Bizim hedefimiz olabildiğince öbek sayısını az seçerek en küçük hata kare toplamı elde etmektir.

sse={}
for k in range(1, 21):
kmeans = KMeans(n_clusters=k, random_state=1)
kmeans.fit(data_normalized)
sse[k] = kmeans.inertia_
plt.figure(figsize=(10,6))
plt.title('The Elbow Method')
# Add X-axis label "k"
plt.xlabel('k')
# Add Y-axis label "SSE"
plt.ylabel('SSE')
# Plot SSE values for each key in the dictionary
sns.pointplot(x=list(sse.keys()), y=list(sse.values()))
plt.text(4.5,60,"Largest Angle",bbox=dict(facecolor='lightgreen', alpha=0.5))
plt.show()

Bu örneğimizde öbek sayımızın 6 olduğunu görüyoruz.

Dirsek metodu
kmeans = KMeans(n_clusters=6, random_state=1) 
kmeans.fit(data_normalized)
cluster_labels = kmeans.labels_RFM_FINAL['Cluster'] = cluster_labels

Algoritmamızı 6 öbekle çalıştırıp müşterilerimifzi kümeledikten sonra aynı tablo içerisinde ‘Cluster’ özniteliği içerisinde kaydediyoruz. Böylece tablomuzun son çıktısı şu hali alıyor:

Sonuç

Böylece yazımızda müşteri segmentasyonu için RFM analizinin ne olduğunu, nasıl yorumlanabileceğini öğrendikten sonra Kaggle’daki Sample Sales Data’yı kullanarak müşterileri bilgilerini, ürünü alım tarihlerini, sayılarını ve miktarlarını kullanarak skorlama işlemimizi yaptık. Daha sonra KMeans algoritmasından ve yüzeysel olarak matematiğinden bahsettikten sonra bazı değişkenlerin dağılımlarını inceleyerek alacağımız sonucun başarısını yükseltmek için logaritma dönüşümü gibi yöntemler uyguladık. Optimal bir küme sayısı bulmak için dirsek metodunun ne olduğundan bahsettik ve kendi verimiz için bu değeri bulduktan sonra algoritmamızı eğiterek sonuçlarını bir DataFrame’de topladık.

Okuduğunuz için teşekkürler, umarım yazım faydalı olabilmiştir :)

--

--

Emre Yüksel
KaVe
Writer for

Data Scientist @ Getir | Computer Engineering MSc Student @ Bogazici University