녕녀기의 실험일지

[ kaggle ] cosmetics ecommerce 분석 : RFM 고객 세분화 분석 본문

-- Data --/- 데이터 분석 -

[ kaggle ] cosmetics ecommerce 분석 : RFM 고객 세분화 분석

녕녀기 2024. 7. 29. 14:48

목차

1. 개요

2. EDA

3. Python 코드로 RFM 고객 세분화

4. BigQuery로 일부 데이터 RFM 고객 세분화

5.고객 세분화 기준 세우기

    5-1. 통계치를 보고 임의의 구간을 나눔

    5-2. 군집 기법 활용 

6. 분석 / 인사이트

7. 배운 점, 어려웠던 점

    7-1. 배운 점

    7-2. 어려웠던 점


1. 개요

  • 가격 데이터가 음수인 데이터 처리 여부 결정
  • 해당 분석을 위해 Python 코드로 구현
  • BigQuery를 통해, SQL로 RFM 쿼리 일부 구현
  • RFM(Recency, Frequency, Monetary) 분석을 위한 기준을 정함
    • '관심 필요 최고 충성 고객'은 '최고 충성 고객'에 준하는 서비스 제공이 필요하다고 생각

2. EDA

df = csv_to_df('ecommerce_data_2019-2020.csv')

df.info()

df.head()

# 통계치 확인

df.describe(include='all')

 

위 코드를 통해 통계치를 확인했을 때, 가격이 음수인 부분을 확인할 수 있었습니다.

Fig 1. price 컬럼의 통계치

 

제품 가격이 음수인 이유를 두 가지 정도 생각해 볼 수 있습니다.

 

1). 특정 제품이나 일정 가격 구매시, 해당 제품의 할인을 음수로 표현했다.

2). 오류이다.

 

1) 검증을 위해 가격이 음수인 제품이 가격 변화를 살펴 봤습니다.

# price가 음수인 경우?
neg_price = df[df['price'] < 0]

# 연월일만 추출
neg_price['event_time_ymd'] = neg_price['event_time'].dt.strftime('%Y-%m-%d')

# 날짜에 따른 가격 변동 확인(해당 제품은 계속 음수인가?)

neg_id = neg_price['product_id'].unique()

fig, axes = plt.subplots(figsize=(8,20), nrows=len(neg_id), ncols=1)

for i in tqdm(range(len(neg_id))) :

  df_neg_id_i = neg_price[neg_price['product_id'] == neg_id[i]]

  axes[i].plot_date(x=df_neg_id_i['event_time'], y=df_neg_id_i['price'])

  axes[i].set_title(f'price trends of product_id_{neg_id[i]}')

  axes[i].set_ylabel('$')

plt.show()

Fig 2. 가격이 음수인 제품들의 가격 변화

 

제품의 가격이 시간이 지나도 일정한 것을 알 수 있습니다. 이 제품은 적어도 이 데이터 셋 안에서는 양수였던 적이 없다는 뜻입니다. 할인의 개념으로 제품 가격을 음수로 계산했는지 확인하기 위해, 음수 제품을 구매했던 사용자들의 총 구매 금액을 살펴 보겠습니다.

# 가격이 음수인 제품이랑 같이 구매된 제품은?

df_neg_id = df[df['product_id'].isin(neg_id)]

neg_session = df_neg_id['user_session'].unique()

neg_user = df_neg_id['user_id'].unique()

df_neg_purchase = df[(df['user_session'].isin(neg_session)) & (df['user_id'].isin(neg_user)) & (df['event_type']=='purchase')]

# user_id별, user_seesion별 총 구매 금액
neg_sum = df_neg_purchase.groupby(['user_id', 'user_session'])['price'].sum().reset_index(level=[0,1], drop=False)

plt.rcParams['font.family'] = 'NanumBarunGothic'

plt.plot(neg_sum['user_id'], neg_sum['price'])
plt.plot(neg_sum['user_id'], [0 for i in range(neg_sum['price'].shape[0])], color='r', linestyle='dashed')

plt.title('가격이 음수인 제품과 같이 구매한 제품들의 총 금액')

plt.xlabel('user_id')

plt.ylabel('sum price')

plt.show()

Fig 3. 가격이 음수인 제품을 같이 구매했던 사용자의 총 구매 금액

 

붉은 선은 구매 금액이 0인 지점인데 총 구매 금액이 음수인 지점을 확인할 수 있습니다. 만약 할인의 개념으로 가격을 음수로 표현했다면, 총 구매 금액이 음수인 경우는 없어야 한다고 생각합니다. 따라서 해당 제품의 가격은 오류로 판단되므로 RFM 고객 세분화 분석에서 제외하도록 하겠습니다. 


3. Python 코드로 RFM 고객 세분화

# 가격이 양수가 아닌 이벤트의 비율

df[df['price'] <= 0].shape[0] / df.shape[0]

# >> 0.005039810871779804

 

가격이 음수인 데이터는 전체 데이터의 0.05% 정도이므로 제외하겠습니다.

# 비율이 전체 이벤트 데이터에 0.5% 이므로 제거해도 무방하다고 판단

df2 = df[df['price'] > 0]

df2.reset_index(drop=True, inplace=True)

 

그리고 RFM 고객 세분화를 위한 코드를 Python으로 작성해 보겠습니다.

 

2020년 3월 1일에 분석을 진행한다고 가정했을 때, 3월 1일 이전 데이터에서 가장 최근에 발생한 Purchase의 날짜를 추출하겠습니다.

# 2020년 3월 1일을 기준으로 사용자의 rfm을 계산

class rfm :

  def __init__(self, dataframe, ts : str = '2020-03-01 00:00:00'):

    self.ts = ts

    dataframe2 = dataframe[(dataframe['event_type'] == 'purchase') & (dataframe['event_time'] <= pd.Timestamp(self.ts, tz='UTC'))]
    dataframe2['event_time1'] = dataframe2['event_time'].dt.strftime('%Y-%m-%d')
    dataframe2['event_time2'] = dataframe2['event_time'].dt.strftime('%Y-%m')
    dataframe2.sort_values(by='event_time', ascending=True, inplace=True)

    self.dataframe = dataframe2

  def recent_date(self) :

    df_recency = self.dataframe.drop_duplicates(subset='user_id', keep='last')

    df_recency = df_recency[['user_id', 'event_time']]

    df_recency.rename(columns={'event_time' : 'recency'}, inplace=True)

    df_recency.reset_index(drop=True, inplace=True)

    self.df_recency = df_recency

    return self.df_recency

  def count_active(self) :

    # 사용자별, 일별로 제품의 구매 여부를 count

    df_frequency = self.dataframe.drop_duplicates(subset=['user_id', 'product_id', 'event_time1'], keep='last')

    df_frequency = df_frequency.groupby(['user_id', 'product_id'])['event_time1'].count()

    df_frequency = df_frequency.reset_index(level=[0, 1], drop=False).groupby(['user_id'])['event_time1'].sum()

    df_frequency.rename('frequency', inplace=True)

    df_frequency = df_frequency.reset_index(drop=False)

    self.df_frequency = df_frequency

    return self.df_frequency

  def sum_price(self) :

    df_sum = self.dataframe.groupby('user_id')['price'].sum()

    df_sum.rename('monetary', inplace=True)

    df_sum = df_sum.reset_index(drop=False)

    self.df_sum = df_sum

    return self.df_sum

  def join_rfm(self) :

    df_recency = self.recent_date()
    df_frequency = self.count_active()
    df_sum = self.sum_price()

    df_join = df_recency.merge(df_frequency, on='user_id').merge(df_sum, on='user_id')

    df_join = df_join.sort_values(by=['frequency', 'monetary', 'recency'], ascending=[False, False, False], axis=0)

    self.df_join = df_join

    return self.df_join

# rfm table
df_rfm = rfm(df2).join_rfm()

df_rfm.info()
df_rfm.head()

 

Fig 4. RFM 고객 세분화 테이블 일부(with Python)

 

위와 같이 테이블을 구축할 수 있습니다.


4. BigQuery로 일부 데이터 RFM 고객 세분화

이번에는 위 과정과 동일하게 BigQuery로 RFM 고객 세분화를 진행해 보겠습니다. 2019년 10월 데이터만 사용해 보겠습니다.

with data_rfm as (
  select event_time, user_id, product_id, event_type, price
  from `kaggle-ecommerce-419203.ecommece_data.2019-Oct`
  where 1=1
    and event_type = 'purchase'
    and event_time < '2020-03-01 00:00:00'
    and price > 0
                )
, recency as (
  select user_id
    ,max(event_time) as recency
  from data_rfm
  group by user_id
            )
, frequency as (
  select user_id
    , sum(frequency) as frequency
  from (
    select user_id
      , product_id
      , count(distinct format_date('%Y-%m-%d', event_time)) as frequency
    from data_rfm
    group by user_id, product_id
        )
  group by user_id
                )
, monetary as (
  select user_id, 
    sum(price) as monetary
  from data_rfm
  group by user_id
)

select r.user_id as user_id
  , r.recency as recency
  , f.frequency as frequency
  , m.monetary as monetary
from recency r
  inner join frequency f
  on r.user_id = f.user_id
  inner join monetary m
  on r.user_id = m.user_id
order by frequency desc, monetary desc, recency desc
;

Fig 5. RFM 고객 세분화 테이블 일부(with BigQuery)

 

Fig 6. 10월 RFM을 Python으로 출력. (Fig 5)와 동일


5. 고객 세분화 기준 세우기

고객 세분화의 기준은 다음과 같이 설정하려고 합니다.

  1. 통계치를 보고 임의로 구간을 나눈 뒤, 고객 세분화
  2. 군집(clustering) 기법을 활용하여, 고객 세분화
    • KMeans 활용
    • DBSCAN 활용

위 과정은 CRM을 담당하는 부서와 협의 끝에 설정한 기준이라고 가정하겠습니다.


5 - 1. 통계치를 보고 임의의 구간을 나눔

# recency, frequncy, monetary를 boxplot으로 표현

col_list = ['recency', 'frequency', 'monetary']

fig, axes = plt.subplots(figsize = (15, 6), nrows=1, ncols=3)

for i in range(3) :

  col = col_list[i]
  ax = axes[i]

  if col == 'recency' :
    ax.boxplot(df_rfm[col].astype(int))
    ax.set_ylabel('datetime to int')

  else :
    ax.boxplot(df_rfm[col])

    if col == 'frequency' :
      ax.set_ylabel('#')
    elif col == 'monetary' :
      ax.set_ylabel('$')

  ax.set_title(f'box plot of {col}')

plt.show()

Fig 7. RFM 데이터의 boxplot
Fig 8. RFM 데이터의 통계치

 

recency는 정규분포에 가까운 형태를 하고 있고, frequency와 monetary는 오른꼬리가 긴 극단적인 형태를 하고 있습니다. frequency와 monetary가 높은 사람들은 이상치로 취급될만큼 보통 사용자와 극명한 차이를 보이는 것으로 확인됩니다.

 

위 통계치와 조사 결과를 통해 고객 세분화의 기준을 세워보겠습니다.


 

보통 recency의 기준을 사내에서 정한 이탈 기준으로 정할 수 있지만, 해당 데이터로 이탈 기준을 정할 수는 없습니다. 따라서, recency의 기준은 3개월 기준으로 나누겠습니다.

3개월인 이유는, 앞선 프로젝트에서 첨부한 조사 결과에서 2~3개월마다 적어도 1회 화장품을 구입하는 비율이 42%이기 때문입니다. 약 절반 정도의 사람이 2~3개월에 한번씩 화장품을 구매한다면, 3개월이 지나도 활성화되지 않은 사용자는 이탈로 판단할 수 있을 것이라고 생각합니다.

 

(사내에서 정한 이탈 기준이 있다면 해당 기준을 따릅니다.)

( iqr = 3사분위수 - 1사분위수 )

 

recency 분류

  • 최근 접속일이 3개월 이내일 경우, RECENT
  • 최근 접속일이 3개월을 초과했을 경우, PAST

frequency의 기준은 frequency의 3사분위수를 기준으로 하겠습니다.

 

frequency 분류

  • 활성 빈도가 13(3사분위수) 이상인 경우, HIGH
  • 활성 빈도가 13(3사분위수) 미만인 경우, LOW

monetary는 monetary의 1사분위수와 3사분위수로 iqr(3사분위수 - 1사분위수)을 계산하여 기준을 정하겠습니다.

 

monetary 분류

  • 총 결제 금액이 3사분위수 + 1.5iqr 이상인 경우, HIGH
  • 총 결제 금액이 33.22(2사분위수) 이상이고 3사분위수 + 1.5iqr 미만인 경우, MIDDLE
  • 총 결제 금액이 33.22(2사분위수) 미만인 경우, LOW

monetary만 3가지로 분류하는 이유는 총 결제 금액이 frequency 보다 중요하다고 생각하기 때문입니다. 모든 고객이 동등하지 않기 때문에, (3사분위수 + 1.5iqr) 이상의 금액을 쓰는 사용자를 상위 등급으로 표현하고자 합니다.


위 기준으로 나눴을 때 고객을 12가지로 분류할 수 있습니다.

1. R = RECENT, F = HIGH, M = HIGH : 최고 충성 고객
2. R = RECENT, F = LOW, M = HIGH : 충성 고객
3. R = RECENT, F = HIGH, M = MIDDLE : 충성 고객
4. R = RECENT, F = HIGH, M = LOW : 일반 고객
5. R = RECENT, F = LOW, M = MIDDLE : 일반 고객
6. R = RECENT, F = LOW, M = LOW : 신규 고객
7. R = PAST, F = HIGH, M = HIGH : 관심 필요 최고 충성 고객
8. R = PAST, F = LOW, M = HIGH : 관심 필요 충성 고객
9. R = PAST, F = HIGH, M = MIDDLE : 관심 필요 충성 고객
10. R = PAST, F = HIGH, M = LOW : 이탈 위험 고객
11. R = PAST, F = LOW, M = MIDDLE : 이탈 위험 고객
12. R = PAST, F = LOW, M = LOW : 이탈 고객

12가지 경우의 수를 통해, 고객을 8개로 분류할 수 있었습니다.

'최고 충성 고객', '충성 고객', '일반 고객', '신규 고객', '관심 필요 최고 충성 고객', '관심 필요 충성 고객', '이탈 위험 고객', '이탈 고객'

 

으로 고객을 분류해 보겠습니다.

# rfm을 분류하기

df_des = df_rfm.iloc[:, 1:].describe(include='all')

# iqr 계산
iqr = df_des.loc['75%', 'monetary'] - df_des.loc['25%', 'monetary']

max_monetary = df_des.loc['75%', 'monetary'] + 1.5 * iqr

# 한 달을 28일로 두고, 3월 1일에서 84일 차이를 기준으로 recency를 나눔
df_rfm['class_recency'] = df_rfm.apply(lambda x : 'RECENT' if (pd.Timestamp('2020-03-01 00:00:00', tz='UTC') - x['recency']).days <= 84 else 'PAST', axis=1)
df_rfm['class_frequency'] = df_rfm['frequency'].apply(lambda x : 'HIGH' if x >= df_des.loc['75%', 'frequency'] else 'LOW')

def class_mon(row) :
  if row['monetary'] >= max_monetary :
    return 'HIGH'
  elif df_des.loc['50%', 'monetary'] <= row['monetary'] < max_monetary :
    return 'MIDDLE'
  else :
    return 'LOW'

df_rfm['class_monetary'] = df_rfm.apply(class_mon, axis=1)
# 분류된 rfm을 통해 고객 분류

def segment_user(row) :
  if row['class_recency'] == 'RECENT' :
    if row['class_frequency'] == 'HIGH' and row['class_monetary'] == 'HIGH' :
      return '최고 충성 고객'
    elif (row['class_frequency'] == 'LOW' and row['class_monetary'] == 'HIGH') or (row['class_frequency'] == 'HIGH' and row['class_monetary'] == 'MIDDLE'):
      return '충성 고객'
    elif (row['class_frequency'] == 'HIGH' and row['class_monetary'] == 'LOW') or (row['class_frequency'] == 'LOW' and row['class_monetary'] == 'MIDDLE'):
      return '일반 고객'
    else :
      return '신규 고객'
  elif row['class_recency'] == 'PAST' :
    if row['class_frequency'] == 'HIGH' and row['class_monetary'] == 'HIGH' :
      return '관심 필요 최고 충성 고객'
    elif (row['class_frequency'] == 'LOW' and row['class_monetary'] == 'HIGH') or (row['class_frequency'] == 'HIGH' and row['class_monetary'] == 'MIDDLE'):
      return '관심 필요 충성 고객'
    elif (row['class_frequency'] == 'HIGH' and row['class_monetary'] == 'LOW') or (row['class_frequency'] == 'LOW' and row['class_monetary'] == 'MIDDLE'):
      return '이탈 위험 고객'
    else :
      return '이탈 고객'

df_rfm['segmentation'] = df_rfm.apply(segment_user, axis=1)

df_rfm.head()

Fig 9. 고객 세분화 완료


5 - 2. 군집 기법 활용

KMeans와 DBSCAN에 대해서 간단하게 살펴보겠습니다.

 

KMeans는 군집 내 크기와 군집 간 거리를 통해 군집을 분류합니다. 군집 내에서 분류된 데이터의 거리는 가까울수록, 군집 간의 거리는 클수록 좋습니다. 보통 군집을 원형으로 분류합니다.

 

DBSCAN은 가깝고 연결돼 있는 데이터끼리 군집을 분류합니다. 비원형의 데이터를 군집화하는데 좋고, 이상치가 포함돼 있어도 군집화하는 기능이 뛰어납니다.


먼저 KMeans기법을 사용하기 위해 RFM 테이블을 전처리 해 줍니다.

df_rfm2 = df_rfm[['user_id','recency', 'frequency', 'monetary']]

# 2020년 3월 1일과의 날짜 차이를 계산
df_rfm2['recency_diff'] = (pd.Timestamp('2020-03-01 00:00:00', tz='UTC') - df_rfm2['recency']).dt.days

 

그 후 RobustScaler를 사용해 변수를 변환해 줍니다.

(해당 Scaler를 사용한 이유는 frequency와 monetary의 데이터 분포가 꼬리가 긴 분포이기 때문입니다.)

(정규 분포 X)

from sklearn.preprocessing import RobustScaler

r_scale = RobustScaler()

r_scale.fit(df_rfm2.iloc[:,2:])

X_train = r_scale.transform(df_rfm2.iloc[:,2:])

 

그 후 elbow method를 사용해 비용이 급격하게 줄어드는 구간을 확인해 봅니다.

# elbow method에서 비용함수가 급격히 줄어드는 k 찾기

from yellowbrick.cluster import KElbowVisualizer

k=0

kmeans = KMeans(n_clusters=k, random_state=26)

visualizer = KElbowVisualizer(kmeans, k=(1,12), timings=False)

visualizer.fit(X_train)
visualizer.show()

Fig 10. elbow method

또한 실루엣 계수도 계산해 적합한 k값을 찾아봅니다.

# 실루엣 계수로 찾기
from sklearn.metrics import silhouette_score

for k in range(2, 12):
    kmeans = KMeans(n_clusters=k, random_state=26).fit(X_train)
    silhouette_avg = silhouette_score(X_train, kmeans.labels_)
    print(f'cluster : {k} // silhouette index {silhouette_avg}')
    
# >> cluster : 2 // silhouette index 0.7872550662319724
# >> cluster : 3 // silhouette index 0.6551884792548721
# >> cluster : 4 // silhouette index 0.5685331191800215
# >> cluster : 5 // silhouette index 0.4716960015948851
# >> cluster : 6 // silhouette index 0.41203076733370236
# >> cluster : 7 // silhouette index 0.3325260195410557
# >> cluster : 8 // silhouette index 0.33346292507591235
# >> cluster : 9 // silhouette index 0.3386224883311785
# >> cluster : 10 // silhouette index 0.3347089984395965
# >> cluster : 11 // silhouette index 0.336696553370974

 

elbow method와 실루엣 계수를 통해 적절한 k 값이 '3'임을 알았습니다. 하지만 다양한 소비 패턴을 보이는 고객들을 겨우 3가지 고객군으로 분류하는 것은 적합한 전략이 아니라고 판단됩니다.


다음은 DBSCAN을 통해 군집화를 진행해 보겠습니다.

 

DBSCAN의 하이퍼 파라미터 중 eps는 KNN(K Nearest Neighbor) 기법을 활용하여 이웃 간 거리가 급격히 증가하는 거리를 파라미터 값으로 사용합니다.

# knn으로 eps 값 찾기

from sklearn.neighbors import NearestNeighbors

# K-NN 거리 계산
k = 5
nbrs = NearestNeighbors(n_neighbors=k).fit(X_train)
distances, indices = nbrs.kneighbors(X_train)

# k번째 이웃까지의 거리 정렬
distances = np.sort(distances[:, k-1], axis=0)

# K-NN 거리 그래프 그리기
plt.plot(distances)
plt.xlabel('Data Points sorted by distance')
plt.ylabel('k-NN distance')
plt.title('K-NN distance graph')
plt.show()

Fig 11. KNN으로 급격히 이웃 간 거리가 증가하는 지점 찾기

 

눈으로 어림 잡아 거리가 '1'인 시점부터 급격히 증가하는 것으로 보입니다. 따라서 eps를 약 1로 대입해 군집화를 진행하겠습니다. 이후 군집화된 데이터를 3D로 시각화 해 봤습니다.

from sklearn.cluster import DBSCAN
from mpl_toolkits.mplot3d import Axes3D

# 1 이상인 값 중 최소값
arr_eps = distances[np.where(distances > 1)][0]

# DBSCAN 클러스터링 수행
db = DBSCAN(eps=arr_eps, min_samples=k).fit(X_train)

# 결과 시각화
labels = db.labels_

df_rfm2['cluster'] = labels

fig = plt.figure(figsize=(10, 10))

plt.rcParams['font.family'] = 'NanumBarunGothic'

ax = fig.add_subplot(111, projection='3d')

# 3D 산점도

for cluster in df_rfm2['cluster'].unique():
    cluster_data = df_rfm2[df_rfm2['cluster'] == cluster]
    ax.scatter(cluster_data['frequency'], cluster_data['monetary'], cluster_data['recency_diff'],
                alpha=1, label=f'Cluster {cluster}')

# 축 레이블 설정
ax.set_xlabel('frequency')
ax.set_ylabel('monetary')
ax.set_zlabel('recency_diff')

plt.show()

Fig 12. DBSCAN으로 군집화된 데이터의 3D 차트

 

Fig 12를 봤을 때, 빨간색으로 분류된 데이터가 너무 많은 것 같습니다.

 

테이블을 통해 다시 살펴보면,

Fig 13. 분류된 고객 수

 

cluster 1에 해당하는 사용자가 압도적으로 많으므로(전체 고객의 99%), 올바르게 분류됐다고 볼 수 없습니다. 

 

따라서 KMeans와 DBSCAN으로 고객 세분화를 하지 않고, 4 - 1에서 분류했던 방법으로 고객을 분류하는 것이 적합해 보입니다.


6. 분석 / 인사이트

각 고객군별로 분류된 사용자의 수와 고객군별 총 매출액을 한번 살펴보겠습니다.

# rfm group by
df_rfm_gr = df_rfm.groupby('segmentation').agg({'user_id' : 'count', 'monetary':'sum'}).reset_index(drop=False).sort_values('monetary', ascending=False)

# 매출 총합
sum_sales = df_rfm_gr['monetary'].sum()

df_rfm_gr['cum_sum'] = df_rfm_gr['monetary'].cumsum() # 누적합

df_rfm_gr['ratio'] = df_rfm_gr['cum_sum'] / sum_sales * 100 # 누적합 / 전체 매출

# 이전 행과 차이가 전체 매출에서 얼마만큼 차지하는지 비율
df_rfm_gr['ratio_diff'] = (df_rfm_gr['monetary'].diff() * -1).fillna(0) / sum_sales * 100

df_rfm_gr

Fig 10. 고객 세분화 테이블

 

위 테이블을 pareto chart로 표현해 보겠습니다.

fig, ax1 = plt.subplots(figsize=(15,6))

plt.rcParams['font.family'] = 'NanumBarunGothic'

ax1.bar(df_rfm_gr['segmentation'], df_rfm_gr['monetary'], color='#B3F6FF')
ax1.set_title('고객 세분화에 따른 매출 파레토 그림')
ax1.set_ylabel('매출액 ($)')

ax2 = ax1.twinx()
ax2.plot(df_rfm_gr['segmentation'], df_rfm_gr['ratio'], color='#C485FF')
ax2.set_ylabel('%')

for i in range(df_rfm_gr['segmentation'].shape[0]):
    ax2.text(df_rfm_gr['segmentation'][i], df_rfm_gr['ratio'][i], str(round(df_rfm_gr['ratio'][i],1))+'%', ha='center', va='bottom')
    ax2.text(df_rfm_gr['segmentation'][i], df_rfm_gr['ratio'][i], '(-'+str(round(df_rfm_gr['ratio_diff'][i],1))+'%)', ha='center', va='top')

plt.show()

Fig 11. 고객 세분화 pareto chart

 

Fig 10과 Fig11을 보면, 당연하게도 최고 충성 고객의 매출이 전체 매출의 30%를 차지할 정도로 많습니다. 매출의 약 80%를 차지하는 고객군은 관심 필요 충성 고객까지라고 볼 수 있을 것 같습니다. 


충성 고객에 가깝게 분류할 수록 구매에 대한 망설임이 적을 것이라고 생각합니다.

 

세분화된 고객 분류별로 purchase / view 이벤트 비율을 살펴보면,

# 5개월간 ecommerce 데이터에서 일부 컬럼만 추출
df_ver_mer = df[['event_time', 'event_type', 'product_id', 'user_id', 'user_session']]

# rfm 테이블에서 일부 컬럼만 추출
df_rfm_ver_mer = df_rfm[['user_id', 'segmentation']]

# merge
df_mer = pd.merge(left = df_ver_mer, right = df_rfm_ver_mer, on = 'user_id', how='inner')

# group by
df_mer_gr = df_mer.groupby(['segmentation', 'event_type'])['user_session'].count().reset_index(level=[0,1], drop=False)


# pivot
df_mer_gr = df_mer_gr.pivot(index = 'segmentation', columns='event_type', values='user_session')[['view', 'purchase']].sort_values('purchase', ascending=False)

Fig 12. 고객 세분화별 이벤트 수

 

카이제곱 독립성 검정을 통해 세분화된 고객 분류와 이벤트 발생 간의 독립성을 살펴보겠습니다.

# 카이제곱 검정을 통한 독립성 검정

chi2 = chi2_contingency(observed=df_mer_gr)

# 유의수준 5%에서

if chi2[1] < 0.05 : 
  print(f'유의수준 5%에서 p-value가 {chi2[1]}이므로 귀무가설 기각, 고객 분류와 view / purchase 이벤트 간의 상관성이 있을 수도 있습니다.')
else : 
  print(f'유의수준 5%에서 p-value가 {chi2[1]}이므로 귀무가설 채택, 고객 분류와 view / purchase 이벤트는 독립적입니다.')

# >> 유의수준 5%에서 p-value가 0.0이므로 귀무가설 기각, 고객 분류와 view / purchase 이벤트 간의 상관성이 있을 수도 있습니다.

 

해당 검정을 통해 세분화된 고객 분류와 이벤트 발생 간의 상관성이 있을지도 모른다고 판단했습니다. 이벤트의 발생 비율을 시각화 해보면,

df_mer_gr['purchase / view'] = df_mer_gr['purchase'] / df_mer_gr['view'] * 100

plt.rcParams['font.family'] = 'NanumBarunGothic'

fig = plt.figure(figsize=(15,8))

plt.barh(y=df_mer_gr.sort_values(by='purchase / view',ascending=True).index, width = df_mer_gr['purchase / view'].sort_values(ascending=True))
plt.title('고객 세분화별 purchase / view (%)')
plt.xlabel('%')

plt.show()

Fig 13. 고객 세분화별 purchase / view (%)

 

충성 고객일 수록 purchase 이벤트 비율이 높다는 것을 확인할 수 있습니다.

 

다만 Fig 11과 Fig 13을 통해 특이한 점을 발견할 수 있었습니다. '관심 필요 최고 충성 고객'이 높은 purchase 이벤트 발생율을 보인다는 것입니다. 해당 고객들이 활발히 구매를 하고 접속을 하지 않는 이유는, 2019년 11월에 있었던 것으로 추정되는 할인 이벤트에서 충분한 구매를 했기 때문으로 생각됩니다.

 

'관심 필요 최고 충성 고객'의 마지막 접속일로 히스토그램을 그려보면,

df_need_att = df_mer[(df_mer['segmentation'] == '관심 필요 최고 충성 고객') & (df_mer['event_type'] == 'purchase')][['event_time', 'user_id', 'segmentation']]

df_need_att['ymd'] = df_need_att['event_time'].dt.strftime('%Y-%m-%d')

df_need_att = df_need_att.groupby('user_id')['ymd'].max().reset_index(drop=False)

import matplotlib.dates as mdates

# datetime 데이터를 숫자로 변환
df_need_att['ymd_num'] = mdates.date2num(df_need_att['ymd'])

fig, ax = plt.subplots(figsize=(15,6), nrows=1, ncols=1)

plt.rcParams['font.family'] = 'NanumBarunGothic'

ax.hist(df_need_att['ymd_num'], bins=10, alpha=1, color='#B8FFCC')

# x축을 datetime 형식으로 설정
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))

ax.set_title('관심 필요 최고 충성 고객의 마지막 접속일 histogram')
ax.set_ylabel('#')

plt.show()

Fig 14. 관심 필요 최고 충성 고객의 마지막 접속일 히스토그램

 

상당수의 고객들이 11월 말부터 12월 초까지만 접속 후 더이상 활동하지 않는 것을 확인할 수 있습니다.


관심 필요 최고 충성 고객은 현재 활동을 하지 않아 총 매출은 적지만, 제품을 구매하는데 있어 주저함이 없는(고민하는 시간이 적은) 고객군이라고 생각이 듭니다.

 

"따라서, 이탈 가능성이 있는 고객군 중 '관심 필요 최고 충성 고객'은 취향에 적합한 제품이라면 바로 구매할 수 있도록, 푸시나 메시지, 이메일에 제품 추천이 필요할 것으로 생각됩니다. 또한 '최고 충성 고객'에게 제공하는 서비스(포인트나 할인 쿠폰, 사은품 등)에 준하는 서비스를 제공할 것이라는 내용을 포함하는 것도 고려해야 한다고 생각합니다."


7. 배운 점, 어려웠던 점

7 - 1. 배운 점

RFM에 대한 개념을 배웠고, SQL과 Python으로 분류하는 방법을 익힐 수 있었습니다. 특히 SQL과 Python에서 동등한 결과가 나오는 것을 보고, 의도한대로 잘 만든 것 같다는 생각을 가지게 됐습니다. 

 

군집 분석에 대한 기법을 사용해 볼 수 있었습니다. 예전부터 KMeans를 주로 사용해 왔지만, 비교를 위해 DBSCAN도 사용해 봤습니다. 군집 평가하는 방식이 두 기법이 다르다는 것을 알게 됐습니다. 비록 두 방법으로 고객을 세분화할 수는 없었지만, 적절한 상황이 생겼을 때 군집화를 통해 분류할 수 있을 것 같습니다.

7 - 2. 어려웠던 점

고객을 세분화하는 기준을 세우는 것이 어려웠습니다. Recency의 경우 3개월을 기준으로 나눴지만, 실제 고객이 3개월만에 이탈했다고 볼 수는 없었습니다. 사내에서 이탈의 기준을 정하는 것의 중요성을 느꼈습니다.

 

처음 Frequency 기준을 세울 때 MAU를 기준으로 정할까 고민했었습니다. 하지만 최소 Active가 1회, 최대 Active가 5회였기 때문에 고객을 구분하기에는 적합하지 않다는 것을 알았습니다. DAU를 사용하고 싶었지만, MAU와 DAU가 같은 고객이 많았기 때문에 DAU도 사용할 수 없었습니다. 이후 같은 제품이라도 여러 날짜로 나눠서 사는 경우도 있는 것을 알게 돼서, 제품 구매 횟수를 Frequency의 기준으로 삼게 됐습니다.

 

Monetary도 처음에는 3사분위수를 기준으로 나누려고 했지만, vip에 대한 대우는 다른 고객과는 달라야 한다는 것을 알게 돼서, 3사분위수 + 1.5 iqr을 초과하는 고객에게 상위 등급을 부과하게 됐습니다.

 

RFM 고객 세분화는 CRM을 관리하는 부서에서 기준을 제공받고, 해당 기준에 따라 분류만 해 주는 것이 적합한 것 같습니다.


참조

https://datarian.io/blog/what-is-rfm

 

RFM 고객 세분화 분석이란 무엇일까요

CRM 타겟팅을 하는 방식 중 가장 범용적으로 사용할 수 있는 RFM 고객 세분화 분석에 대해 알아보겠습니다

datarian.io

https://brunch.co.kr/@d43dde8758de4a9/35

 

클릭하고 싶은 CRM. 앱 푸시 8가지 법칙 !

CRM 알아가기 시리즈 2편 - 클릭율 높은 푸시의 비밀 | CRM 앱 푸시의 목적은 <CRM 알아가기 시리즈 1편>을 통해서 설명했듯이 일반적으로 푸시, 문자, 메일을 통해서 리텐션을 높이고 구매전환으

brunch.co.kr

 

Comments