Kwon's 데이터분석기

Daicon 경진대회 - 학습 플랫폼 이용자 구독 갱신 예측 EDA 및 통계분석 본문

데이터 분석

Daicon 경진대회 - 학습 플랫폼 이용자 구독 갱신 예측 EDA 및 통계분석

DataKwon 2023. 12. 22. 11:19

Daicon 학습 플랫폼 이용자 구독 갱신 예측 해커톤이 열려서 참여를 하였다.

(데이터 출처: https://dacon.io/competitions/official/236179/overview/description)

 

코딩 환경은 Jupyter Notebook을 통해서 수행하였다.

 

먼저 기본적으로 필요한 모듈을 들고와준다.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
from matplotlib import rc
rc('font', family = 'Malgun Gothic')
plt.rcParams['axes.unicode_minus']= False

 

그 후 저장해둔 데이터 csv파일을 들고와준다.

train_data = pd.read_csv('./dataset/subscription/train.csv')
test_data = pd.read_csv('./dataset/subscription/test.csv')
result_data = pd.read_csv('./dataset/subscription/sample_submission.csv')

 

이렇게 들고온 데이터의 열이 무엇이 있는지 파악을 하기 위해 .info()를 사용하여 출력해준다.

print(train_data.info(), '\n')
print(test_data.info())

불러온 데이터의 열

 

* 데이터의 열의 내용

subscription_duration: 사용자가 서비스에 가입한 기간 (월)

recent_login_time: 사용자가 마지막으로 로그인한 시간 (일)

average_login_time: 사용자의 일반적인 로그인 시간

average_time_per_learning_session: 각 학습 세션에 소요된 평균 시간 (분)

monthly_active_learning_days: 월간 활동적인 학습 일수

total_completed_courses: 완료한 총 코스 수

recent_learning_achievement: 최근 학습 성취도

abandoned_learning_sessions: 중단된 학습 세션 수

community_engagement_level: 커뮤니티 참여도

preferred_difficulty_level: 선호하는 난이도

subscription_type: 구독 유형

customer_inquiry_history: 고객 문의 이력

payment_pattern 사용자의 지난 3개월 간의 결제 패턴을 10진수로 표현한 값.

  • 7: 3개월 모두 결제함
  • 6: 첫 2개월은 결제했으나 마지막 달에는 결제하지 않음
  • 5: 첫 달과 마지막 달에 결제함
  • 4: 첫 달에만 결제함
  • 3: 마지막 2개월에 결제함
  • 2: 가운데 달에만 결제함
  • 1: 마지막 달에만 결제함
  • 0: 3개월 동안 결제하지 않음

target: 사용자가 다음 달에도 구독을 계속할지 (1) 또는 취소할지 (0)를 나타냄

 

자세한 열의 목록은 이렇게 이루어져 있다. 이를 통해서 데이터에 대해서 EDA를 수행할 것이다.

 

열 중에서는 user_id 라는 열이 존재하는데 이는 개개인의 사람들에 대한 고윳값을 나타내기 때문에 EDA 및 데이터분석,  모델링을 할 때 불필요하기 때문에 이를 삭제를 수행한다.

# train, test_data에 있는 id는 고유값이라서 모델학습때 사용하지 않으므로 삭제
train_data.drop('user_id', axis = 1, inplace = True)
test_data.drop('user_id', axis = 1, inplace = True)

그 후 데이터에 대해서 중복되는 값이 있는지 확인을 해본다.

train_data[train_data.duplicated()] #train_data에 대해서 중복되는 값이 있는지 확인

train_data에 중복값 존재 여부

확인 결과 데이터에 중복되는 값이 존재하지 않는 것을 확인할 수 있다.

 

다음으로 수치형 데이터의 열과 범주형 데이터의 열을 리스트 형태로 저장한 후, train_data에서

수치형 데이터의 통계치를 확인을 해본다.

# 수치형 데이터 열의 이름을 리스트형태로 col_num에 저장
col_num = train_data.select_dtypes(exclude='object').columns.tolist()
# 범주형 데이터 열의 이름을 리스트 형태로 col_cat에 저장
col_cat = train_data.select_dtypes(include='object').columns.tolist()

# train_data중 수치형에 대한 통계치 파악
train_data[col_num].describe()

train_data의 수치형 데이터 통계치

여기서 확인해 볼수 있는 것은

최근 로그인한 시간이 29일 전인 사람이 존재

-> 29일 전이면 이 사람은 학습을 다한것인지중도 포기한것인지 확인 필요

 

평균 로그인 시간이 2.3분인 사람이 존재

-> 평균 로그인 시간이 2.3분이면 이 사람은 학습에 참여를 제대로 한 것인가?

 

학습 세션 소요된 평균 시간이 거의 0분인 사람이 있고, 503분이 걸린 사람이 존재한다

-> 503분이 소요된 사람은 학습을 제대로 참여한 것이 맞는지 확인이 필요하다.

 

완료된 총 코스의 수가 1인 사람이 존재

-> 왜 1개를 들었는지? 확인이 필요하다. 최근 가입인원일 가능성 존재

 

그 후 수치형 데이터에 대한 분포를 파악해보기 위해 히스토그램을 그려본다.

# 수치형 데이터에 대한 히스토그램 출력
i = 0
plt.figure(figsize=(12, 8))

for col in col_num:
    i += 1
    plt.subplot(4,3, i)
    sns.distplot(train_data[col], bins= 50)
plt.tight_layout()

수치형 데이터에 대한 히스토그램

히스토그램을 살펴보니 average_login_time과 recent_learning_achievement, total_completed_courses가 정규분포를 따르는 것으로 보이며, submission_duration과 recent_login_time, monthly_active_learning_days는 균등분포를 따르는 것으로 보인다.

나머지 average_time_per_learning_sessions는 오른쪽으로 꼬리가 긴 양수 왜도가 나타났으며, abandoned_learning_sessions과 customer_inquiry_history는 데이터가 float 정수형이 아닌 int 수치형이기 때문에 정규분포를 따르는 것일지라도 히스토그램의 뿔이 여러개가 나오는 것으로 보이는 것 같다.

그 외 나머지 데이터들은 히스토그램으로 데이터를 파악하기엔 적합하지 않아보인다.

 

이러한 정보 이외에 알아볼 수 있는 정보로는 target 변수가 불균형하게 차이가 존재하는 것을 파악할 수 있으며payment_pattern은 고르게 나타나있고, community_engagement_level가 높은 사람이 많다는 것을 알 수 있다.

 

데이터가 정규분포를 따르는지에 대해서 통계치로 알아보기 위해서 주로 사용하는 통계검정은 Shapiro_wilk검정이지만, Shapiro_wilk 정규성 검정은 데이터의 개수가 5000개 이상으로 넘어가면, 신뢰성이 떨어지기 때문에 다른 유사한 검정인 앤더슨-달링 검정을 사용하도록 한다.

from scipy.stats import anderson #앤더슨 달링 검정을 사용하기 위한 모듈 들고오기

for col in col_num:
    result = anderson(train_data[col])
    
    print(f"Results for {col} column: ")
    print("Statistic: ", result.statistic)
    print("Critical Values: ", result.critical_values)
    print("Significance Levels: ", result.significance_level)
    if result.statistic > result.critical_values[2]:
        print(f"{col}은 정규분포를 따르지 않습니다.", '\n')
    else:
        print("f{col}은 정규분포를 따릅니다.", '\n')

수치형 데이터에 대한 앤더슨 달링 정규성 검정

아래에 다른 수치형에 대한 앤더슨-달링 정규성 검정의 결과가 나와있다. 정리를 해보면

*정규분포 따르는 데이터

average_login_time, recent_learning_achievement 

 

*정규분포를 따르지 않는 데이터

subscription_duration, recent_login_time, average_time_per_learning_session, monthly_active_learning_days, total_completed_courses, abandoned_learning_sessions, community_engagement_level, customer_inquiry_history, payment_pattern, target 으로 이루어져 있었다.

 

정규분포를 따르지 않는 데이터 중 payment_pattern, community_engagement_level은 수치형 데이터로 되어있지만 실상 범주형 변수 형태이며, target은 우리가 예측을 해야하는 종속변수이므로 정규분포를 따르지 않았다고 하더라도 문제가 없다.

 

그러므로 payment_pattern과 community_engagement_level은 범주형으로 변환해준다.

train_data['payment_pattern'] = train_data['payment_pattern'].astype('object')
train_data['community_engagement_level'] = train_data['community_engagement_level'].astype('object')

 customer_inquiry_history 변수도 얼핏보면 범주형처럼 보이지만, 문의를 넣은 횟수라는 수치형 데이터이기 때문에 변환을 시켜주지 않았다.

이렇게 변한 col_num과 col_cat 이라는 수치, 범주형 변수 열을 리스트에 저장한 값을 갱신을 해준다.

# 앞에 지정한 col_num과 col_cat 갱신
col_num = train_data.select_dtypes(exclude='object').columns.tolist()
col_cat = train_data.select_dtypes(include='object').columns.tolist()

 

이제 커뮤니티 참여도에 따른 다른 변수들간의 차이를 확인해보기 위해 막대그래프를 그려본다.

i = 0
plt.figure(figsize = (18, 12))

for col in col_num:
    i += 1
    plt.subplot(6,2, i)
    sns.barplot(x = train_data['community_engagement_level'], y = train_data[col],data= train_data)

    plt.tight_layout()
plt.show()

커뮤니티 참여도 별 수치형 변수에 대한 막대그래프

community_engagement_level에 따른 수치형 변수에 대한 차이를 확인해보니, 대부분의 변수에서는 차이가 없다고 나타났지만, average_time_per_learning_session과 total_completed_courses에서는 차이가 존재했다.

 

먼저 community_engagement_level가 낮을수록 total_completed_courses가 적다는 것을 파악을 할 수 있고, average_time_per_learning_session도 community_engagement_level이 낮을수록 수치가 낮다는 것을 알 수 있다.

 

그 다음으론 선호난이도 preferred_difficulty_level별 막대그래프로 비교를 해볼 것이다.

i = 0
plt.figure(figsize = (18, 12))

for col in col_num:
    i += 1
    plt.subplot(6,2, i)
    sns.barplot(x = train_data['preferred_difficulty_level'], y = train_data[col],data= train_data)

plt.tight_layout()
plt.show()

선호난이도 별 수치형 변수에 대한 막대그래프

막대그래프를 확인해보니 다른 변수와는 차이가 나는 것은 확인이 되지 않지만, average_time_per_learning_session과  total_completed_courses가 차이가 존재하는 것으로 보인다.

선호 난이도가 높을수록 total_completed_courses가 낮아지는 것을 확인할 수 있고, average_time_per_learning_sessions도 선호 난이도가 높을 수록 낮게 나왔다. 일반적이라면 preferred_difficulty_level이 높을수록average_time_per_learning_session이 높아야 할 것 같지만 낮게 나와 나의 예상과는 다른 값이었다.

 

그래서 한번 고민해본 결과 자신의 선호 난이도가 높으면 그 사람은 그 분야에 대해서 숙달된 사람일 것이라 생각을 하였으며, 숙달된 만큼 average_time_per_learning_session이 낮게 나온것이라 생각하게 되었다.

 

마지막으로는 구독타입 subscription_type별 수치형 변수에 대한 막대그래프를 그려볼 것이다.

i = 0
plt.figure(figsize = (18, 12))

for col in col_num:
    i += 1
    plt.subplot(6,2, i)
    sns.barplot(x = train_data['subscription_type'], y = train_data[col],data= train_data)

plt.tight_layout()
plt.show()

구독 타입 별 수치형 변수에 대한 막대그래프

막대그래프를 확인해보니 다른 막대그래프와 같이 다른변수에는 차이가 없지만 average_time_per_learning_session과 total_completed_courses에서만 차이가 존재했다.

total_completed_courses는 구독이 premium인 사람이 basic인 사람보다 높게 나왔는데, 이는 모든 구독 가격이 basic보다 premium이 더 높기 때문에 학습을 하는 사람들은 자신이 사용을 한 돈에 따라 참여율에 차이가 존재한다는 것을 알 수 있었으며, average_time_per_learning_session또한 premium인 사람이 참여율이 더 높기 떄문에 premium이 더 높을 수 밖에 없었을 것이다.

 

*그래프만으로는 불안할 수 있기 때문에 통계적 방법도 섞어서 맞는지 확인해본다.

구독 여부에 따른 완료 코스 수의 차이를 검정해본다.

# 통계검정 -> 난이도 선택에 따른 코스 완료수 차이 검정
from scipy.stats import f_oneway
group_v1 = train_data[train_data['preferred_difficulty_level']=='Low']['total_completed_courses']
group_v2 = train_data[train_data['preferred_difficulty_level']=='Medium']['total_completed_courses']
group_v3 = train_data[train_data['preferred_difficulty_level']=='High']['total_completed_courses']

f_statistic, p_value = f_oneway(group_v1, group_v2, group_v3)
print(f'F-Statistic: {f_statistic}, P-Value : {p_value}')

난이도 선택에 따른 코스 완료수 차이 검정

p-value값이 4.3195e-187로 유의수준 0.05하에 귀무가설이 기각이 되므로 난이도 선택에 따른 완료 코스 수에는 차이가  존재하는 것을 알 수 있다. 이러한 방법으로 차이가 존재하는지 확인해본다.

또한 어떤 그룹 간에 차이가 나는지 확인할 수도 있다.

# 터커의 다중비교 검정
from statsmodels.stats.multicomp import pairwise_tukeyhsd

tukey_results = pairwise_tukeyhsd(endog=train_data['total_completed_courses'],
                                 groups=train_data['preferred_difficulty_level'],
                                 alpha = 0.05)

print(tukey_results)

터커의 다중비교 검정

이러한 검정을 통해 선호난이도가 High와 Medium은 차이가 없지만, High와 Low, Medium과 Low에는 차이가 존재한다는 것을 알 수 있다.

 

이제 수치형 데이터에 대해서 boxplot을 그려보면서 이상치가 존재하는지 간접적으로 확인해 볼 것이다.

# 수치형 데이터에서 boxplot을 그려 이상치가 있는지 간접적으로 파악해본다.
i = 0
plt.figure(figsize = (18, 12))

for col in col_num:
    i += 1
    plt.subplot(4,3, i)
    sns.boxplot(x = col, data = train_data, palette = 'husl')

plt.tight_layout()
plt.show()

수치형 변수에 대한 boxplot(target 미포함)

boxplot을 해본 결과 데이터에 대한 이상치가 존재할 것 같은 데이터는 average_time_per_learning_session, average_login_time, total_completed_courses, recent_learning_achievement, abandoned_learning_sessions, customer_inquiry_history가 보인다.

이러한 이상치는 데이터에 대한 IQR 기준으로써의 이상치이므로 이상치가 아닌 이상치 후보일 뿐 진짜 이상치가 아닐수도 있다는 것이다.

이러한 이상치를 정확하게 확인해보기 위해서 train_data에서 test_data셋의 최대 최소값을 기준으로 값이 벗어나는게 존재하는지 확인하여 진짜 이상치를 한번 탐지해보도록 한다.

이러한 방법으로 이상치를 검사하기전 train_data와 test_data의 분포가 유사한지 검증을 한 후 분포가 유사하다면 위 방법을 사용해서 이상치를 탐지할 수 있다.

 

먼저 train_data셋과 test_data셋의 분포에 차이가 있는지에 대해서 K-S 검정을 수행한다.

#어떤 변수에서 차이 있는가?
from scipy import stats

feature_list = test_data.columns.values.tolist()
for feature in feature_list:
    statistics, p_value = stats.kstest(train_data[feature], test_data[feature])
    if statistics > 0.1 and p_value < 0.05:
        print(f'K-S Test Value: {statistics:.2f}, with p-value {p_value:.2f}, the feature, {feature}')

train_data, test_data의 K-S 검정 결과

train_data, test_data의 K-S검정 결과 p-value 0.00으로 유의수준 0.05하 귀무가설을 기각, 대립가설을 채택하므로 train_data, test_data간의 분포에 차이가 존재한다. train_data와 test_data의 분포에 차이가 존재하는 변수는 preferred_difficulty_level에서 차이가 존재한다는 것을 알 수 있다.

즉, preferred_difficulty_level을 제외한 나머지 변수들에서는 분포에 대한 차이가 없다는 것을 뜻한다.

 

이제 train_data에 test_data의 최대 최소값을 기준으로 벗어나는 값이 존재하는지 확인을 해보도록 한다.

#train, test set에서 사용할 변수 지정
features = ['subscription_duration', 'recent_login_time', 'average_login_time',
       'average_time_per_learning_session', 'monthly_active_learning_days',
       'total_completed_courses', 'recent_learning_achievement',
       'abandoned_learning_sessions', 'community_engagement_level',
       'preferred_difficulty_level', 'subscription_type',
       'customer_inquiry_history', 'payment_pattern']

# train_data의 히스토그램을 그린 후 test_data의 변수별 최대 최소값을 선으로 표시함
f, ax = plt.subplots(5, 3, figsize=(10, 10))
ax = ax.ravel()
for ax, f in zip(ax, features):
    ax.hist(train_data[f], bins=100, density=True)
    ax.set_ylabel('density')
    
    ax.axvline(test_data[f].min(), color='r')
    ax.axvline(test_data[f].max(), color='r')
    
plt.tight_layout()
plt.show()

train_data에 대한 히스토그램과 test_data의 변수별 최대 최소값의 선

train_data에 대한 히스토그램에서 test_data의 변수별 최대 최소값의 선을 보니 선을 벗어나는 값이 존재하지 않아 보인다.

이를 통해서 이상치값이 딱히 없어보인다는 것을 알 수 있었다. 그러므로 데이터 삭제 또는 변환을 수행하지 않는다.

 

이로써 데이터에 대해서 알수 있는 정보는

1. train_data와 test_data에서의 분포는 preferred_difficulty_level을 제외하고 차이가 없다.

2. average_time_per_learning_session은 오른쪽으로 꼬리가 긴 양수 왜도 형태를 띈다.

3. train_data의 boxplot결과에서는 이상치가 존재하는 것 처럼 보였지만 train_data에서 test_data의

   최대 최소값에 벗어나는 값이 존재하는지 확인해보니 없기 때문에 이상치가 따로 존재하지 않는 것 같다.

4. 커뮤니티 참여도가 낮을수록 총 학습 코스 수가 적다.

5. 커뮤니티 참여도가 낮을수록 각 학습 세션에 소요된 평균 시간 도 낮다.

6. 선호 난이도가 높을수록 완료한 총 코스 수가 낮다.

7. 선호 난이도가 높을수록 각 학습 세션에 소요된 평균 시간이 낮다.

8. 완료한 총 코스 수는 구독 유형이 Premium인 사람이 Basic인 사람보다 높다.

9. 각 학습 세션에 소요된 평균 시간은 Premium인 사람이 Basic인 사람보다 높다.

10. target변수에 대해서 데이터 불균형이 존재한다.

 

 

이렇게 데이콘 학습플랫폼 구독 갱신 여부 예측 데이터에 대한 EDA 및 통계 분석을 마쳤다.

다음에는 이러한 EDA 기반으로 데이터를 전처리를 하여 모델을 만들도록 하겠다.