ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 실전 예제 - 마케팅 데이터 분석 04 (Revenue - 02)
    빅데이터/Data-Analysis 2022. 2. 24. 15:58

    패스트캠퍼스 '직장인을 위한 파이썬 데이터분석 올인원 패키치 Online' 참조

     

     

    • 지금까지 K군집으로 고객세그먼트를 나누어 보았다.
    • 또 다른 방법의 하나로 '실루엣'이라는 계수가 있다. 
    • 실루엣 계수를 구하는 방법은 각 샘플의 클러스터(군집) 내부 거리의 평균 (a)와 인접 클러스터와의 거리 평균(b)를 사용하여 계산. 즉 (b-1) / max(a,b) 로 계산
    • 가장 좋은 값은 1, 최악은 -1로 나옴. 0 근처의 값은 클러스터가 오버랩이 되었다는 의미이며 음수값은 샘플이 잘못된 클러스터에 배정이 되었다거나 다른 클러스터가 더 유사한 군집이라는 의미
    • 실무에서는 도메인에 따라서 스코어와 상관없이 적절한 K가 있을 수 도 있음

     

    01 실루엣 계수 보기

    from sklearn.metrics import silhouette_samples, silhouette_score
    
    # 클러스터의 갯수 리스트를 만들어줍니다. 
    range_n_clusters = [2, 3, 4, 5, 6, 7, 8]
    
    # 사용할 컬럼 값을 지정해줍니다. 
    X = df[['Age', 'Annual Income (k$)', 'Spending Score (1-100)']].values
    
    
    for n_clusters in range_n_clusters:
        # 1 X 2 의 서브플롯을 만듭니다. 
        fig, (ax1, ax2) = plt.subplots(1, 2)
        fig.set_size_inches(18, 7)
    
        # 첫 번째 서브플롯은 실루엣 플롯입니다. 
        # silhouette coefficient는 -1에서 1 사이의 값을 가집니다.
        # 시각화에서는 -1 값이 잘 나오지 않음
        # -0.1에서 1사이로 지정해줍니다. 
        ax1.set_xlim([-0.1, 1])
    
        # clusterer를 n_clusters 값으로 초기화 해줍니다.  
        # 재현성을 위해 random seed를 10으로 지정 합니다.
        clusterer = KMeans(n_clusters=n_clusters, random_state=10)
        cluster_labels = clusterer.fit_predict(X)
    
        # silhouette_score는 모든 샘플에 대한 평균값을 제공합니다. 
        # 실루엣 스코어는 형성된 군집에 대해 밀도(density)와 분리(seperation)에 대해 견해를 제공합니다. 
        silhouette_avg = silhouette_score(X, cluster_labels)
        print("For n_clusters =", n_clusters,
              "The average silhouette_score is :", silhouette_avg)
    
        # 각 샘플에 대한 실루엣 스코어를 계산합니다. 
        sample_silhouette_values = silhouette_samples(X, cluster_labels)
    
        y_lower = 10
        for i in range(n_clusters):
            # 클러스터 i에 속한 샘플들의 실루엣 스코어를 취합하여 정렬합니다. 
            ith_cluster_silhouette_values = \
                sample_silhouette_values[cluster_labels == i]
    
            ith_cluster_silhouette_values.sort()
    
            size_cluster_i = ith_cluster_silhouette_values.shape[0]
            y_upper = y_lower + size_cluster_i
    
            color = cm.nipy_spectral(float(i) / n_clusters)
            ax1.fill_betweenx(np.arange(y_lower, y_upper),
                              0, ith_cluster_silhouette_values,
                              facecolor=color, edgecolor=color, alpha=0.7)
    
            # 각 클러스터의 이름을 달아서 실루엣 플롯의 Label을 지정해줍니다. 
            ax1.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
    
            # 다음 플롯을 위한 새로운 y_lower를 계산합니다.
            y_lower = y_upper + 10  # 10 for the 0 samples
    
    
    
    
    
    
        ax1.set_title("The silhouette plot for the various clusters.")
        ax1.set_xlabel("The silhouette coefficient values")
        ax1.set_ylabel("Cluster label")
    
        # 모든 값에 대한 실루엣 스코어의 평균을 수직선으로 그려줍니다. 
        ax1.axvline(x=silhouette_avg, color="red", linestyle="--")
    
        ax1.set_yticks([])  # yaxis labels / ticks 를 지워줍니다. 
        ax1.set_xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])
    
        # 두 번째 플롯이 실제 클러스터가 어떻게 형성되었는지 시각화 합니다. 
        colors = cm.nipy_spectral(cluster_labels.astype(float) / n_clusters)
        ax2.scatter(X[:, 0], X[:, 1], marker='.', s=30, lw=0, alpha=0.7,
                    c=colors, edgecolor='k')
    
        # 클러스터의 이름을 지어줍니다. 
        centers = clusterer.cluster_centers_
        # 클러스터의 중앙에 하얀 동그라미를 그려줍니다. 
        ax2.scatter(centers[:, 0], centers[:, 1], marker='o',
                    c="white", alpha=1, s=200, edgecolor='k')
    
        for i, c in enumerate(centers):
            ax2.scatter(c[0], c[1], marker='$%d$' % i, alpha=1,
                        s=50, edgecolor='k')
    
        ax2.set_title("The visualization of the clustered data.")
        ax2.set_xlabel("Feature space for the 1st feature")
        ax2.set_ylabel("Feature space for the 2nd feature")
    
        plt.suptitle(("Silhouette analysis for KMeans clustering on sample data "
                      "with n_clusters = %d" % n_clusters),
                     fontsize=14, fontweight='bold')
    
    plt.show()

    클러스터가 6일때 가장 계수가 좋게 나왔다.

     

     

     

    02 해석 및 적용 방안

    • 각각의 데이터들이 클러스터를 통해 클러스터 어디에 속할지 번호를 부여받았다.

    • 이제 이것들은 DF열에 따로 추가해서 어떤 고객이 어떤 클러스터들을 가지고 있는지 구분 지은 후 다시 특성을 파악 해야 한다.
    # 기존 데이터 셋에 각 고객이 속한 클러스터 값을 넣은 후 특성 확인
    df['cluster'] = cluster_labels
    df

    df.groupby('cluster')['Age'].mean()
    cluster
    0    27.000000
    1    41.685714
    2    56.155556
    3    25.272727
    4    32.692308
    5    44.142857
    
    sns.boxplot(x='cluster',y='Age',hue='Gender',palette=['c','m'], data=df);

    • 군집별로 나이 평균을 박스 플롯으로 표현 했다.
    • 여러 그래프로 비교 해 보자
    # 바이올린과 스웜플롯으로 구체적으로 시각화
    ax = sns.violinplot(x='cluster', y='Annual Income (k$)', data=df, inner=None)
    ax = sns.swarmplot(x='cluster', y='Annual Income (k$)', data=df, color='white', edgecolor='grey')
    
    ax = sns.violinplot(x='cluster', y='Spending Score (1-100)', data=df, inner=None)
    ax = sns.swarmplot(x='cluster', y='Spending Score (1-100)', data=df, color='white', edgecolor='grey')
    
    ax = sns.violinplot(x='cluster', y='Age', data=df, inner=None)
    ax = sns.swarmplot(x='cluster', y='Age', data=df, color='white', edgecolor='grey')

    • 3차원도 있지만 3차원은 보기가 힘들다 (아래 참고)

    ▶ 적용 방안 논의

    더보기
    더보기
    더보기

    # 한번에 놓고 적용 방안 논의
    # 3개의 시각화를 한 화면에 배치합니다. 
    figure, ((ax1, ax2, ax3)) = plt.subplots(nrows=1, ncols=3)

    # 시각화의 사이즈를 설정해줍니다. 
    figure.set_size_inches(20, 6)

    # 클러스터별로 swarmplot을 시각화해봅니다. 
    ax1 = sns.violinplot(x="cluster", y='Annual Income (k$)', data=df, inner=None, ax=ax1)
    ax1 = sns.swarmplot(x="cluster", y='Annual Income (k$)', data=df,
                       color="white", edgecolor="gray", ax=ax1)

    ax2 = sns.violinplot(x="cluster", y='Spending Score (1-100)', data=df, inner=None, ax=ax2)
    ax2 = sns.swarmplot(x="cluster", y='Spending Score (1-100)', data=df,
                       color="white", edgecolor="gray", ax=ax2)

    ax3 = sns.violinplot(x="cluster", y='Age', data=df, inner=None, ax=ax3)
    ax3 = sns.swarmplot(x="cluster", y='Age', data=df,
                       color="white", edgecolor="gray", ax=ax3, hue="Gender")

    • 각 클러스터 별 특성을 아래와 같이 정리를 한 후 

    • 소비가 가장 높은 군집은 1번과 4번. 이 둘을 타겟으로 방안 마련
    • 1번의 군집 특성
      연간소득이 높은편, 잘 유도 하여 지출을 더 하게 만들 수 있음
      1의 연령대는 20대 후반 ~ 40대 초반으로 젊은 편
      1의 세부 고객 정보를 더 분석해서 타겟 마케팅을 기획 즉 고소득 젊은층이 선호할 이벤트나 사은품등을 기획
    • 4번 군집
      연간 소득은 낮지만 우리 쇼핑몰에서 소비점수는 높은 편
      특히 우리 쇼핑몰에 대한 충성도가 높고 구매 비율이 높은 고객군으로 추정
      가격적인 혜택을 추가 제공하는것을 고려(할인 쿠폰, 멤버십 등)


    • 잠재 VIP 고객 - 2번: 연간소득은 높지만 소비점수가 낮은 케이스
      우리 쇼핑몰에서 소비를 더 할 수 있는 고객 층
      연령은 전 연령대에 걸쳐 있고 다른 변수들을 넣어 잠재성과 고객의 방문, 구매를 유도해볼필요가 있음
      재 방문시 사은품 증정, 특정 금액 이상 구매시 혜택 제공 등 재 방문과 구매를 유도 할 수 있음

     

     

     

     

     

     

     

     

     

    ♣ Revenue 예제 2

     

    01 문제 정의

    • 이익을 극대화하는 또 하나의 예제 '통신사 고객 데이터'를 가지고 연습해보자. 
    https://www.kaggle.com/blastchar/telco-customer-churn -> 파일 다운 가능

    * 기본 세팅

    import pandas as pd
    import numpy as np 
    import matplotlib.pyplot as plt
    import seaborn as sns
    from sklearn.cluster import KMeans
    
    df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
    df.head()
    
    df.shape
    (7043, 21)

    * 각 콜럼 설명

    • 해지 여부
      • Churn - 고객이 지난 1개월 동안 해지했는지 여부 (Yes or No)
    • Demographic 정보
      • customerID - 고객들에게 배정된 유니크한 고객 번호 입니다.
      • gender - 고객의 성별 입니다(male or a female).
      • Age - 고객의 나이 입니다.
      • SeniorCitizen - 고객이 senior 시민인지 여부(1, 0).
      • Partner - 고객이 파트너가 있는지 여부(Yes, No).
      • Dependents - 고객이 dependents가 있는지 여부(Yes, No).
    • 고객의 계정 정보
      • tenure - 고객이 자사 서비스를 사용한 개월 수.
      • Contract - 고객의 계약 기간 (Month-to-month, One year, Two year)
      • PaperlessBilling - 고객이 paperless billing를 사용하는지 여부 (Yes, No)
      • PaymentMethod - 고객의 지불 방법 (Electronic check, Mailed check, Bank transfer (automatic), Credit card (automatic))
      • MonthlyCharges - 고객에게 매월 청구되는 금액
      • TotalCharges - 고객에게 총 청구된 금액
    • 고객이 가입한 서비스
      • PhoneService - 고객이 전화 서비스를 사용하는지 여부(Yes, No).
      • MultipleLines - 고객이 multiple line을 사용하는지 여부(Yes, No, No phone service).
      • InternetService - 고객의 인터넷 서비스 사업자 (DSL, Fiber optic, No).
      • OnlineSecurity - 고객이 online security 서비스를 사용하는지 여부 (Yes, No, No internet service)
      • OnlineBackup - 고객이 online backup을 사용하는지 여부 (Yes, No, No internet service)
      • DeviceProtection - 고객이 device protection에 가입했는지 여부 (Yes, No, No internet service)
      • TechSupport 고객이 tech support를 받고있는지 여부 (Yes, No, No internet service)
      • StreamingTV - 고객이 streaming TV 서비스를 사용하는지 여부 (Yes, No, No internet service)
      • StreamingMovies - 고객이 streaming movies 서비스를 사용하는지 여부 (Yes, No, No internet service)

     

     

    ▶ 문제 정의

    • 대부분 데이터가 범주형으로 되어 있다
    • 통신사의 고객 데이터에서 CLV를 계산하여 고객의 CHURN-해지여부를 예측을 해 볼 예정
    • CLV(Customer Lifetime Value)로 고객 생애 가치로 불리는데 고객이 우리 서비스를 이요하는 총 기간 내에 얼마만큼 이익을 주는가를 돈으로 계산한것. 

    • M - 고객 1 인당 평균 매출. 보통 1년 단위로 계산
      c - 고객 1인당 평균 비용. 보통 1년 단위로 계산
      r - 고객 유지 비율(Retention rate). 즉 어떤 고객이 그 다음 해에도 여전히 고객으로 남아 있을 확률
      i - 이자율 또는 할인율
      AC - 고객 획득 비용(Acquisition Cost). 고객이 첫 방문 또는 첫 구매를 하도록 하는데 드는 비용
      참조 링크 (https://brunch.co.kr/@yhsonb0r/2)

     

     

     

    02 EDA

    # 데이터 확인
    df.shape
    (7043, 21)
    
    # 결측치
    df.isnull().sum()
    customerID          0
    gender              0
    SeniorCitizen       0
    Partner             0
    Dependents          0
    tenure              0
    PhoneService        0
    MultipleLines       0
    InternetService     0
    OnlineSecurity      0
    OnlineBackup        0
    DeviceProtection    0
    TechSupport         0
    StreamingTV         0
    StreamingMovies     0
    Contract            0
    PaperlessBilling    0
    PaymentMethod       0
    MonthlyCharges      0
    TotalCharges        0
    Churn               0
    
    df.info()
     #   Column            Non-Null Count  Dtype  
    ---  ------            --------------  -----  
     0   customerID        7043 non-null   object 
     1   gender            7043 non-null   object 
     2   SeniorCitizen     7043 non-null   int64  
     3   Partner           7043 non-null   object 
     4   Dependents        7043 non-null   object 
     5   tenure            7043 non-null   int64  
     6   PhoneService      7043 non-null   object 
     7   MultipleLines     7043 non-null   object 
     8   InternetService   7043 non-null   object 
     9   OnlineSecurity    7043 non-null   object 
     10  OnlineBackup      7043 non-null   object 
     11  DeviceProtection  7043 non-null   object 
     12  TechSupport       7043 non-null   object 
     13  StreamingTV       7043 non-null   object 
     14  StreamingMovies   7043 non-null   object 
     15  Contract          7043 non-null   object 
     16  PaperlessBilling  7043 non-null   object 
     17  PaymentMethod     7043 non-null   object 
     18  MonthlyCharges    7043 non-null   float64
     19  TotalCharges      7043 non-null   object 
     20  Churn             7043 non-null   object
    • 대부분의 위에서 언급했던것처럼 범주형으로 되어 있다.
    • 거기다 나머지 범주형은 이해가 가는데 'TotalCharges'가 범주형인게 이상하다 확인 후 변경 해 주자
    df['TotalCharges']
    0         29.85
    1        1889.5
    2        108.15
    3       1840.75
    4        151.65
             ...   
    7038     1990.5
    7039     7362.9
    7040     346.45
    7041      306.6
    7042     6844.5
    • 숫자인데도 object형이다. 'MonthlyCharges'와 같은 형태로 바꿔 주자
    pd.to_numeric(df['TotalCharges'])
    
    ValueError: Unable to parse string " " at position 488
    • 빈값때문에 오브젝트형으로 맞춰진거같다. 빈값을 제거하고 바꿔 주자
    df[df['TotalCharges'] == " "]
    • 이렇게 확인을 해보면 'TotalCharges' 가 공백인 데이터의 'tenure'(고객이 자사제품을 사용한 개월 수)를 보면 0으로 나오는데 이 뜻은 가입한지 아직 한달도 안된 고객이라고 유추가 된다. 즉 아직 한달도 사용을 안했으니 매달 요금도 안나간것으로 간주 된다. 
    • 이 빈값을 'NaN'로 처리하자
    # 빈캅 nan으로 처리
    df['TotalCharges'] = df['TotalCharges'].replace(' ', np.nan)
    
    df[df['tenure'] == 0]

     

     

    • 사실 널값이 아닌 빈공백의 데이터가 11개밖에 되지 않아서 버려도 된다. 이를 버리고 나머지 데이터들의 형태를 바꿔주도록 하자
    # 널값들은 버리고 나머지 형태 바꾸기
    df = df[df['TotalCharges'].notnull()]
    
    # 데이터 형태 바꾸기
    df['TotalCharges'] = df['TotalCharges'].astype(float)
    print(df.shape)
    print(df.info())
    
    (7032, 21)
     #   Column            Non-Null Count  Dtype  
    ---  ------            --------------  -----  
     0   customerID        7032 non-null   object 
     1   gender            7032 non-null   object 
     2   SeniorCitizen     7032 non-null   int64  
     3   Partner           7032 non-null   object 
     4   Dependents        7032 non-null   object 
     5   tenure            7032 non-null   int64  
     6   PhoneService      7032 non-null   object 
     7   MultipleLines     7032 non-null   object 
     8   InternetService   7032 non-null   object 
     9   OnlineSecurity    7032 non-null   object 
     10  OnlineBackup      7032 non-null   object 
     11  DeviceProtection  7032 non-null   object 
     12  TechSupport       7032 non-null   object 
     13  StreamingTV       7032 non-null   object 
     14  StreamingMovies   7032 non-null   object 
     15  Contract          7032 non-null   object 
     16  PaperlessBilling  7032 non-null   object 
     17  PaymentMethod     7032 non-null   object 
     18  MonthlyCharges    7032 non-null   float64
     19  TotalCharges      7032 non-null   float64
     20  Churn             7032 non-null   object
    df.describe()

    # 변수간 corr 확인
    df.corr()
    
    # 시각화
    corr = df.corr()
    sns.heatmap(corr, annot=True);

    • 사용개월수와 차지들간의 관계가 보인다
    • 해지고객의 분포도 보자
    # 해지고객 분포
    sns.countplot(y='Churn', data=df);

     

    # 변수들간의 pairplot
    sns.pairplot(df, hue='Churn');

    • tenure(자사 사용 개월수)가 낮은 경우 churn(해지여부)가 높음. 즉 최근 고객들이 해지를 많이 함
    • 어느정도 이상의 tenure이 되면 충성고객이 되어 churn이 낮게 나옴
    • MonthlyChares가 높을 경우 churn도 높음
    • tenure와 MonthlyChrages가 주요 변수로 보임

     

    • pairplot으로 볼 수 있는 변수들은 한계가 있음. 범주형도 처리를해서 다같이 보도록 하자
    # 카테고리 변수의 카테고리가 어떻게 구성되어 있는지 확인 합니다. 
    print(df['gender'].value_counts())
    print("=================================")
    print(df['Partner'].value_counts())
    print("=================================")
    print(df['Dependents'].value_counts())
    print("=================================")
    print(df['PhoneService'].value_counts())
    print("=================================")
    print(df['MultipleLines'].value_counts())
    print("=================================")
    print(df['InternetService'].value_counts())
    print("=================================")
    print(df['OnlineSecurity'].value_counts())
    print("=================================")
    print(df['OnlineBackup'].value_counts())
    print("=================================")
    print(df['DeviceProtection'].value_counts())
    print("=================================")
    print(df['TechSupport'].value_counts())
    print("=================================")
    print(df['StreamingTV'].value_counts())
    print("=================================")
    print(df['StreamingMovies'].value_counts())
    print("=================================")
    print(df['Contract'].value_counts())
    print("=================================")
    print(df['PaperlessBilling'].value_counts())
    print("=================================")
    print(df['PaymentMethod'].value_counts())
    print("=================================")
    print(df['Churn'].value_counts())

    'No internet service' 를 No로 처리 해도 될듯

    # 다음 컬럼들에 대해 'No internet service'를 'No'로 변환해줍니다. 
    replace_cols = ['MultipleLines', 'OnlineSecurity', 'OnlineBackup', 'DeviceProtection','TechSupport','StreamingTV', 'StreamingMovies']
    for i in replace_cols : 
        df[i]  = df[i].replace({'No internet service' : 'No'})

     

Designed by Tistory.