Miuul Araştırma

Power BI ile CRM Analitiği - IV: Müşteri Yaşam Boyu Değeri (CLTV) Tahmini

CLTV elde etme ile ilgili literatürde çok farklı şekillerde hesaplama modelleri bulunmaktadır. Bu yazıda tahmin etme metodu ile görselleştirme yapıyoruz.


Valentin Tkach

Müşteri yaşam boyu değeri (Customer Lifetime Value) elde etme ile ilgili literatürde çok farklı şekillerde hesaplama modelleri bulunmaktadır. Önceki yazıda hesaplama metodu ile Power BI’da görselleştirmesini yaparken bu yazı da tahmin etme metodu ile görselleştirme yapacağız.

Elimizdeki müşterilerin kullanıcı davranışlarını göz önünde bulundurarak belli bir zaman projeksiyonu ile geleceğe yönelik Customer Life Time Value tahmini yapabiliriz.

Her müşterinin churn olma (terk etme) , dropout olma (kısmi churn olma) durumları birbirinden farklı olabilir. Bu durum da prediction daha doğru bir sonuç verir.

CLTV prediction için kullanılan 2 olasılıksal dağılım;

  • BGNBD (Beta Geometric Negative Binomial Distribution)
  • Gamma Gamma

Her iki dağılıma göre model oluşturup birleştirilerek final model oluşturulur.

BG / NBD : Expected Number of Transaction ile ilgilenirken (Beklenen İşlem Sayısı) ilgili dağılımın içinde zaman değişkeni vardır. Müşterinin satın alma davranışı yapısı model sayesinde öğrenilir. Modele fit ederken Frequency, Recency ve Tenure (müşteri yaşı) değerleri ile fit edilir.

Gamma Gamma : Expected Average Profit değeri ile ilgilenir. (Beklenen Ortalama Karlılık) Modele fit edilirken Frequency ve Monetary değerleri ile fit edilir.

Data müşteri bazında tekil olmalıdır. Her müşterinin;

  • Recency değerini ( son alışveriş yaptığı tarih — ilk alışveriş yatığı tarih)
  • Tenure değerini yani müşteri yaşı ( bugünün tarihi — ilk alışveriş yaptığı tarih)
  • Satın alma adedini
  • Toplam getirdiği geliri hesaplanır.

CLTV = Expected Number of Transaction (BGNBD) * Expected Average Profit (Gamma Gamma)

Veri setimizi artık tanıyoruz. PowerBI’ da python kodu çalıştırabilmek için “Get Data” — “More” tıklayıp arama çubuğuna python yazmamız yeterli. Şimdiye kadar CRM analitiği kapsamında elde ettiğimiz segmentleri final tablomuza ekleyelim. Oluşturduğumuz segmentasyon çalışmaları sırasıyla RFM , KMeans, CLTV_Calculation, CLTV_Prediction segmentleridir.

CRM Final tablosu için aşağıdaki kodu çalıştırabilirsiniz.

import datetime as dt
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import MinMaxScaler
from lifetimes import BetaGeoFitter
from lifetimes import GammaGammaFitter
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
from warnings import simplefilter
simplefilter(action='ignore', category=FutureWarning)
from warnings import simplefilter
simplefilter(action='ignore', category=FutureWarning)

##################################
# Verinin Çağrılması
##################################


df_ = pd.read_excel("C:\online_retail_II.xlsx", sheet_name="Year 2010-2011")
df = df_.copy()

##################################
# Veri Ön İşleme
##################################

def outlier_thresholds(dataframe, variable):
    quartile1 = dataframe[variable].quantile(0.01)
    quartile3 = dataframe[variable].quantile(0.99)
    interquantile_range = quartile3 - quartile1
    up_limit = quartile3 + 1.5 * interquantile_range
    low_limit = quartile1 - 1.5 * interquantile_range
    return low_limit, up_limit

def replace_with_thresholds(dataframe, variable):
    low_limit, up_limit = outlier_thresholds(dataframe, variable)
    # dataframe.loc[(dataframe[variable] < low xss=removed>
    dataframe.loc[(dataframe[variable] > up_limit), variable] = up_limit

def crm_data_prep(dataframe):
    dataframe.dropna(axis=0, inplace=True)
    dataframe = dataframe[~dataframe["Invoice"].str.contains("C", na=False)]
    dataframe = dataframe[dataframe["Quantity"] > 0]
    replace_with_thresholds(dataframe, "Quantity")
    replace_with_thresholds(dataframe, "Price")
    dataframe["TotalPrice"] = dataframe["Quantity"] * dataframe["Price"]
    return dataframe

df_prep = crm_data_prep(df)

##################################
# RFM Segmenti Oluşturma
##################################

def create_rfm(dataframe):
    # RFM METRIKLERININ HESAPLANMASI
    today_date = dt.datetime(2011, 12, 11)
    rfm = dataframe.groupby('Customer ID').agg({'InvoiceDate': lambda date: (today_date - date.max()).days,
                                                'Invoice': lambda num: num.nunique(),
                                                "TotalPrice": lambda price: price.sum()})

    rfm.columns = ['recency', 'frequency', "monetary"]

    rfm = rfm[(rfm['monetary'] > 0)]

    # RFM SKORLARININ HESAPLANMASI
    rfm["recency_score"] = pd.qcut(rfm['recency'].rank(method="first"), 5, labels=[5, 4, 3, 2, 1])
    rfm["frequency_score"] = pd.qcut(rfm["frequency"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5])

    # SEGMENTLERIN ISIMLENDIRILMESI
    rfm['rfm_segment'] = rfm['recency_score'].astype(str) + rfm['frequency_score'].astype(str)

    seg_map = {
        r'[1-2][1-2]': 'hibernating',
        r'[1-2][3-4]': 'at_risk',
        r'[1-2]5': 'cant_loose',
        r'3[1-2]': 'about_to_sleep',
        r'33': 'need_attention',
        r'[3-4][4-5]': 'loyal_customers',
        r'41': 'promising',
        r'51': 'new_customers',
        r'[4-5][2-3]': 'potential_loyalists',
        r'5[4-5]': 'champions'
    }
    rfm['rfm_segment'] = rfm['rfm_segment'].replace(seg_map, regex=True)
    rfm = rfm[["recency", "frequency", "monetary", 'recency_score', 'frequency_score', "rfm_segment"]]
    return rfm

rfm = create_rfm(df_prep)
rfm.recency_score = rfm.recency_score.astype(int)
rfm.frequency_score = rfm.frequency_score.astype(int)

##################################
# KMEANS CLUSTERING
##################################
k_means = KMeans(n_clusters=6, init='k-means++', random_state = 0).fit(rfm[['recency_score', 'frequency_score']])
rfm['K_means_label'] = k_means.labels_
rfm['K_means_label'] = rfm['K_means_label'] + 1


##################################
# CLTV Calculation
##################################

def create_cltv_c(dataframe):
    # avg_order_value
    dataframe['avg_order_value'] = dataframe['monetary'] / dataframe['frequency']

    # purchase_frequency
    dataframe["purchase_frequency"] = dataframe['frequency'] / dataframe.shape[0]

    # repeat rate & churn rate
    repeat_rate = dataframe[dataframe.frequency > 1].shape[0] / dataframe.shape[0]
    churn_rate = 1 - repeat_rate

    # profit_margin
    dataframe['profit_margin'] = dataframe['monetary'] * 0.05

    # Customer Value
    dataframe['cv'] = (dataframe['avg_order_value'] * dataframe["purchase_frequency"])

    # Customer Lifetime Value
    dataframe['cltv'] = (dataframe['cv'] / churn_rate) * dataframe['profit_margin']

    # minmaxscaler
    scaler = MinMaxScaler(feature_range=(1, 100))
    scaler.fit(dataframe[["cltv"]])
    dataframe["cltv_c"] = scaler.transform(dataframe[["cltv"]])

    dataframe["cltv_c_segment"] = pd.qcut(dataframe["cltv_c"], 3, labels=["C", "B", "A"])

    dataframe = dataframe[["recency", "frequency", "monetary", "recency_score", "frequency_score", "rfm_segment",  "K_means_label",
                          "cltv_c", "cltv_c_segment"]]

    return dataframe

rfm_cltv = create_cltv_c(rfm)

##################################
# CLTV Prediction
##################################

def create_cltv_p(dataframe):
    today_date = dt.datetime(2011, 12, 11)

    ## recency kullanıcıya özel dinamik.
    rfm = dataframe.groupby('Customer ID').agg({'InvoiceDate': [lambda date: (date.max()-date.min()).days,
                                                                lambda date: (today_date - date.min()).days],
                                                'Invoice': lambda num: num.nunique(),
                                                'TotalPrice': lambda TotalPrice: TotalPrice.sum()})

    rfm.columns = rfm.columns.droplevel(0)

    ## recency_cltv_p
    rfm.columns = ['recency_cltv_p', 'T', 'frequency', 'monetary']

    ## basitleştirilmiş monetary_avg
    rfm["monetary"] = rfm["monetary"] / rfm["frequency"]

    rfm.rename(columns={"monetary": "monetary_avg"}, inplace=True)

    # BGNBD için WEEKLY RECENCY VE WEEKLY T'nin HESAPLANMASI
    ## recency_weekly_cltv_p
    rfm["recency_weekly_cltv_p"] = rfm["recency_cltv_p"] / 7
    rfm["T_weekly"] = rfm["T"] / 7

    # KONTROL
    rfm = rfm[rfm["monetary_avg"] > 0]

    ## recency filtre (daha saglıklı cltvp hesabı için)
    rfm = rfm[(rfm['frequency'] > 1)]

    rfm["frequency"] = rfm["frequency"].astype(int)

    # BGNBD
    bgf = BetaGeoFitter(penalizer_coef=0.01)
    bgf.fit(rfm['frequency'],
            rfm['recency_weekly_cltv_p'],
            rfm['T_weekly'])

    # exp_sales_1_week
    rfm["exp_sales_1_week"] = bgf.predict(1,  # 1 weeks
                                          rfm['frequency'],
                                          rfm['recency_weekly_cltv_p'],
                                          rfm['T_weekly'])

    # exp_sales_1_month
    rfm["exp_sales_1_month"] = bgf.predict(4, #4 weeks
                                          rfm['frequency'],
                                          rfm['recency_weekly_cltv_p'],
                                          rfm['T_weekly'])
    # exp_sales_3_month
    rfm["exp_sales_3_month"] = bgf.predict(12, #4*3 weeks
                                          rfm['frequency'],
                                          rfm['recency_weekly_cltv_p'],
                                          rfm['T_weekly'])

    # expected_average_profit
    ggf = GammaGammaFitter(penalizer_coef=0.01)
    ggf.fit(rfm['frequency'], rfm['monetary_avg'])
    rfm["expected_average_profit"] = ggf.conditional_expected_average_profit(rfm['frequency'],
                                                                            rfm['monetary_avg'])
    # 6 aylık cltv_p
    cltv = ggf.customer_lifetime_value(bgf,
                                      rfm['frequency'],
                                      rfm['recency_weekly_cltv_p'],
                                      rfm['T_weekly'],
                                      rfm['monetary_avg'],
                                      time=6, #6 months
                                      freq="W", #tenure
                                      discount_rate=0.01)

    rfm["cltv_p"] = cltv

    # minmaxscaler
    scaler = MinMaxScaler(feature_range=(1, 100))
    scaler.fit(rfm[["cltv_p"]])
    rfm["cltv_p"] = scaler.transform(rfm[["cltv_p"]])

    # cltv_p_segment
    rfm["cltv_p_segment"] = pd.qcut(rfm["cltv_p"], 3, labels=["C", "B", "A"])

    # recency_cltv_p, recency_weekly_cltv_p
    rfm = rfm[["recency_cltv_p", "T", "monetary_avg", "recency_weekly_cltv_p", "T_weekly", "exp_sales_1_week",
              "exp_sales_1_month", "exp_sales_3_month", "expected_average_profit",
              "cltv_p", "cltv_p_segment"]]

    return rfm

rfm_cltv_p = create_cltv_p(df_prep)
crm_final = rfm_cltv.merge(rfm_cltv_p, on="Customer ID", how="left")
crm_final = crm_final.reset_index()

crm_final_country = crm_final.merge(df_prep[["Customer ID", "Country"]].drop_duplicates(), on="Customer ID", how="left")

Kodu script olarak aşağıdaki gibi yapıştıralım. Final tablomuz olan crm_final_country’ i seçelim.

Yükleme olduktan sonra datada sayısal değerleri otomatik number’ a çevrilirken nokta virgül uyumsuzlukları olduğunu göreceksiniz. Bu durumu istemediğimizden otomatik dönüştürme işlemini “Applied Steps” kısmından silelim. “Queries” sekmesindeki “Transform Data” ya basalım ve değiştilmesi gereken sütunları tamamen seçip “Any Columns” ta yer alan “Replace Values” e basarak noktayı virgüle çevirelim.

tablo içeren bir resim

Açıklama otomatik olarak oluşturuldu

Dönüştürme ve nokta olarak beslenen değerleri virgül ile yer değiştirme işlemlerinden sonra datanın son hali aşağıdaki gibi olmaktadır. “Applied Steps” kısmında dönüşümler görülebilir.

metin, beyaz, ekran görüntüsü içeren bir resim

Açıklama otomatik olarak oluşturuldu

Dönüşüm işlemleri bittiğinde Close&Apply diyerek query editörden çıkalım.

CLTV prediction için oluşturduğumuz görselleştirme için ilk olarak “Visualization” kısmında yer alan “Slicer” görselini seçelim. Bu görsel sayesinde elde ettiğimiz segmentleri kendi özelinde veya beraber seçerek adet, tutarların değişimini görebiliriz. Slicer’ ın yönünü yatay olarak çevirmek için görseli seçtikten sonra “Format” ta yer alan “General” — “Orientation” u Horizontal seçelim. Bu görselde toplamda 4 adet slicer vardır.

Elde etmiş olduğumuz CLTV_Calculation ve CLTV_Prediction segmentleri sıralanırken A segmenti her ikisinde de en iyi müşteri grubudur.

Diğer satırdaki 3 görsel için sırasıyla “Map” , “Clustered Bar Chart”, “Line and Clustered Column Chart” kullanılmıştır.

Harita için Customer ID adetleri, diğer tablo için beklenen haftalık (exp_sales_1_week) , aylık (exp_sales_1_month) ve 3 aylık (exp_sales_3_month) satış adetleri toplamı vardır. Son tablo içinde Shared Axis’te cltv_p_segment , Column Values’ta ortalama müşteri yaşı (Tenure) ve Recency değeri, Line Values kısmında ise ortalama Frequency değeri karşılaştırılmıştır.

Son görsel satırda ise toplam 4 görsel bulunmaktadır. İlk iki görsel “Gauge” göselinde tek bir ölçünün durumu görselleştirilmiştir. Bunlar sırasıyla 3 aylık beklenen satış adeti ve toplam beklenen karlılıktır. Diğer görselde bu değerler ülkelere göre “Area Chart” kullanılarak görselleştirilmiştir. Son görselde ise RFM segmentleri “Tree Map” kullanılarak görselleştirilmiştir.

Bu yazımızda CRM analitiği kapsamında segmentasyon çalışmaları ayrı ayrı görselleştirilmiştir. Odağımız daha çok CLTV Prediction iken her bir oluşturulan segment sektörde çok kıymetli olup üzerinde daha detaylı görselleştirmeler yapılabilir.

Okuduğunuz için teşekkürler.

Referans: VBO

Etiketler

BigQuery ML ile Çok Terimli Regresyon

Cem İstanbullu

R ile Yüksek Performanslı Programlama - I

Onur Boyar

T-SQL ile 20 Milyon Satırlık Gerçek Müşteri Verisi Üretme

Ömer Çolakoğlu

SON ARAŞTIRMALAR

GİRİŞ

Aşağıya kaydolduğunuz e-postayı girin. Şifrenizi sıfırlamanız için size bir bağlantı göndereceğiz.

Giriş Ekranına Dön
Şifre sıfırlama talimatları
adresine gönderildi.