마케팅 데이터를 분석해 봅시다! 사실 상 가장 빨리 돈이 되는 데이터는 마케팅 데이터입니다. 이런 데이터를 봐 두면 분명 도움이 될 거니까, 참고해 주세요.
마케팅 데이터 분석
: Kaggle Mobile Games A/B Testing
데이터 위치
: https://www.kaggle.com/yufengsui/mobile-games-ab-testing
데이터 설명
userid - 개별 유저들을 구분하는 식별 번호
version - 유저들이 실험군 대조군 중 어디에 속했는지 알 수 있습니다. (gate_30, gate_40 : A/B 테스트 그룹)
gate_30은 30 level에서 게임을 지속 하기 위한 첫 번째 요구(Lock)를 받는 버전
gate_40은 40 level에서 게임을 지속 하기 위한 첫 번째 요구(Lock)를 받는 버전
sum_gamerounds - 첫 설치 후 14일 간 유저가 플레이한 라운드의 수입니다.
retention_1 - 유저가 설치 후 1일 이내에 다시 돌아왔는지 여부입니다.
retention_7 - 유저가 설치 후 7일 이내에 다시 돌아왔는지 여부입니다.
시나리오)
Cookie Cats 게임에서는 특정 스테이지가 되면 스테이지가 Lock되게 합니다. (gate_30은 30번째, gate_40은 40번째 스테이지)
Area Locked일 경우 Keys를 구하기 위한 특별판 게임을 해서 추가 Key를 구하거나, 페이스북 친구에게 요청하거나, 유료아이템을 구매하여 바로 열 수 있습니다. 마치 애니팡 같군요.
Goal!
Lock을 몇 번째 스테이지에서 할 때 이용자 retention에 가장 좋을지 의사결정을 해야합니다.
그 이외에 또 다른 의견이 있는지도 살펴보는 것이 목표입니다.
데이터를 준비하는 과정은 이전에도 많이 해봤으니 입을 다물고 드르륵 해보도록 합시다. (입꾹)
!kaggle datasets download -d yufengsui/mobile-games-ab-testing
!unzip ./mobile-games-ab-testing.zip
import pandas as pd
df_raw = pd.read_csv("cookie_cats.csv")
df_raw.head()
userid | version | sum_gamerounds | retention_1 | retention_7 | |
---|---|---|---|---|---|
0 | 116 | gate_30 | 3 | False | False |
1 | 337 | gate_30 | 38 | True | False |
2 | 377 | gate_40 | 165 | True | False |
3 | 483 | gate_40 | 1 | False | False |
4 | 488 | gate_40 | 179 | True | True |
뭐 데이터가 여러가지 있군요 한번 어떤 식인지 들여다 보시죠.
df_raw.shape
(90189, 5)
df_raw.info()
RangeIndex: 90189 entries, 0 to 90188 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 userid 90189 non-null int64 1 version 90189 non-null object 2 sum_gamerounds 90189 non-null int64 3 retention_1 90189 non-null bool 4 retention_7 90189 non-null bool dtypes: bool(2), int64(2), object(1) memory usage: 2.2+ MB
df_raw.describe()
userid | sum_gamerounds | |
---|---|---|
count | 9.018900e+04 | 90189.000000 |
mean | 4.998412e+06 | 51.872457 |
std | 2.883286e+06 | 195.050858 |
min | 1.160000e+02 | 0.000000 |
25% | 2.512230e+06 | 5.000000 |
50% | 4.995815e+06 | 16.000000 |
75% | 7.496452e+06 | 51.000000 |
max | 9.999861e+06 | 49854.000000 |
후후. 대충~ 보니 결측값은 없고, 좋네요. 그러면 30버전과 40버전 사용자의 분포는 어떻게 되나요?
df_raw['version'].value_counts()
gate_40 45489 gate_30 44700 Name: version, dtype: int64
으흠~ 비슷해 보이네요. 그럼, 얼마나 사용자들이 게임을 많이 하는지 알아봅시다.
df_raw['sum_gamerounds'].hist()
어 이게 머지.. 싶은데.. describe() max값을 보니까 엄청나게 플레이 한 미친 사람이 있군요 ㅠㅠ. 그러면 대~충 10000번 이상한 user는 이상해 보이지 않아요? 이상치로 일단 제거를 하는 편이 나을 것 같습니다.
df_raw = df_raw[df_raw['sum_gamerounds']<10000]
df_raw.describe()
userid | sum_gamerounds | |
---|---|---|
count | 9.018800e+04 | 90188.000000 |
mean | 4.998397e+06 | 51.320253 |
std | 2.883298e+06 | 102.682719 |
min | 1.160000e+02 | 0.000000 |
25% | 2.512211e+06 | 5.000000 |
50% | 4.995804e+06 | 16.000000 |
75% | 7.496461e+06 | 51.000000 |
max | 9.999861e+06 | 2961.000000 |
제거 완료. 다시 한번 대~충 볼까 합니다.
df_raw['sum_gamerounds'].hist()
아니 아직도 좀 이상치가 있나요?!. 우리는 이런 미친 충성고객을 보려는 건 아니니까 이번엔 box plot으로 한번 보시죠
df_raw['sum_gamerounds'].plot.box()
아니 이건 또 뭐랍니까... sum_gamerounds가 좀 선을 넘네요 ㅎㅎ.
개인적인 의견으로는 50회 이하로 제한 하는 편이 비즈니스 로직에 더 맞을 것 같긴 한데요.. 0~50회 정도 Play횟수가 일반적인 사용자의 범위라고 가정! 흣차~ 50에서 잘라봐요!
df_raw = df_raw[df_raw['sum_gamerounds']<50]
df_raw.describe()
userid | sum_gamerounds | |
---|---|---|
count | 6.702400e+04 | 67024.000000 |
mean | 4.996576e+06 | 13.665582 |
std | 2.884096e+06 | 12.963878 |
min | 1.160000e+02 | 0.000000 |
25% | 2.510286e+06 | 3.000000 |
50% | 4.999982e+06 | 9.000000 |
75% | 7.499912e+06 | 21.000000 |
max | 9.999861e+06 | 49.000000 |
df_raw['sum_gamerounds'].hist()
이렇게 이상치를 제거하고 보니까, 조금 이뻐 보이네요. 당연히 Positive Skewed되어 있군요. 대~충 13회 정도가 평균이군요. ★ 역시나 평균이라는 것은 이상치에 너무 많이 좌우되는군요 ★ 그러니까 이럴 때는 중앙값이 중요합니다. 9회정도가 중앙값이군요. 그래도 이정도에서 타협을 보는 것이 좋겠다는 생각이 드네요. 플레이 횟수에 대한 user의 수를 자세히 보면
df_raw.groupby("sum_gamerounds").get_group(0) # 0번 플레이한 user들 이건 그냥 본거에요.
# sum_gamerounds 횟수별 grouping 보고, 그런 user가 몇명 있냐? 이런 식으로 볼 수도 있습니다.
df_raw.groupby("sum_gamerounds")['userid'].count().plot()
요~ 당연한 이야기이겠지만, 1회 플레이가 최대값이고, 0회도 생각보다 엄청 많군요. (설치하자 마자 지움. 자기야 어디가...) 0회가 제일 문제인 것 같은데... 이것에 대한 정보도 부족합니다. 어쨌든 두개 버전이 다운로드 후에 다른 지에 대한 내용이 없어서 그것은 비교하면 안될 것 같고, 1번만 실행한 사람수도 엄청 많군요. 어쨌든 두 버전이 차이가 나는지 한번 보는 것이 낫겠습니다. 두 버전의 데이터를 나눠보는 편이 정식이겠죠?
df_version_30 = df_raw[df_raw['version']=='gate_30'].copy()
df_version_40 = df_raw[df_raw['version']=='gate_40'].copy()
일단 각각의 통계치를 한번 보는 편이 감잡기 좋지 않을까 하는데.. 말이죠
print(df_version_30.describe())
print(df_version_40.describe())
userid sum_gamerounds count 3.337100e+04 33371.000000 mean 4.991735e+06 13.989242 std 2.879695e+06 13.165095 min 1.160000e+02 0.000000 25% 2.510221e+06 3.000000 50% 5.003670e+06 9.000000 75% 7.481820e+06 22.000000 max 9.999710e+06 49.000000 userid sum_gamerounds count 3.365300e+04 33653.000000 mean 5.001377e+06 13.344635 std 2.888487e+06 12.753302 min 4.830000e+02 0.000000 25% 2.510396e+06 3.000000 50% 4.995400e+06 9.000000 75% 7.511397e+06 20.000000 max 9.999861e+06 49.000000
두 버전에 대한 그룹이 대~충 잘 나눠져 있었군요? 엇, 평균이 두 그룹이 비슷~하군요? 21.486401 VS 21.317289, 어. 그리고, 어, 뭐야 표준편차도 비슷합니다. 23.252602 VS 23.626467 이거 두개 그룹이 별로 차이가 안나는거 아닌가 싶은데요.
뭐 대충 각각 그려볼까요?
df_version_30['sum_gamerounds'].hist()
df_version_40['sum_gamerounds'].hist()
역시, 예상대로 대~충 비슷해 보이는군요. 그러면 평균이 차이가 나는지 한번 보자면,
Independent Samples t-Test 가 생각나는군요. 한번 확인해 보시죠.
"2개 집단 간에 차이가 나는가? Independent Samples t-검정 에 대한 심플함" 을 참고하시면 좀 낫겠네요.
표본이 충분히 크고 평균을 비교할 것이기 때문에 Normality 체크는 중심극한정리를 인용하여 건너 뛰도록 하겠습니다.
검정을 하기 위해서 등분산성은 먼저 확인하고요, (bartlett)으로요.
30과 40의 게임 플레이 수에 대한 집단 차이 - 등분산성과 Independent Samples t-Test (플레이 50번까지)
우선 등분산성!!을 확인
from scipy.stats import bartlett
bartlett(df_version_40['sum_gamerounds'], df_version_30['sum_gamerounds'])
BartlettResult(statistic=33.83871195585611, pvalue=5.987549168595243e-09)
Independet Samples t-Test?
Bartlett 등분산성 검정을 해 보니, 등분산이라는 Null Hypothesis를 기각해야 겠군요. 이게 기각되면 중심극한정리를 인용하더라도 표본평균의 분산도 달라져서 곤란합니다. 그러면 곧바로 Welch's Test를 할 수가 있겠는데, Independent Sampels t-Test에 equal_var를 False로 주면 됩니다. 후후.
from scipy.stats import ttest_ind
ttest_ind(df_version_40['sum_gamerounds'], df_version_30['sum_gamerounds'], equal_var=False)
Ttest_indResult(statistic=-6.43745968259507, pvalue=1.2230874242440918e-10)
검정 결과!
아이고 검정결과를 보니, 두 버전사이에 Play횟수의 차이가 유의합니다. 특히 30 version이 평균이 더 크겠군요. 흠. 인터레스팅. 여기에서 문제는 플레이 횟수와 레벨의 관계가 있었다면 훨씬 좋았을텐데 이에 대한 것이 없는 바람에 50을 기준으로 생각하고 있긴 한데, 사실 50회 이내에 Lock 레벨이 없었다면 이건 잘못된 분석입니다. 그건 주의해 주세요. 게다가 play횟수가 일반적으로 더 큰 표본 수로 비교한다면 또 결과가 달라질 수도 있습니다. 어쨌든 이 검정의 가정은 Play 50회 내에 Lock이 대부분 Lock이 걸린다는 가정입니다. 아쉽.
그렇다면, 40버전과 30버전의 각 retention에 대한 차이 분석도 적절하게 필요하겠군요.
이 retention의 경우에는 boolean 데이터라서 t검정이 아니라 cross table을 만들어서 동질성 검정을 하는 것이 좋겠습니다. df_version_40과, df_version_30의 길이가 달라서 곧바로 cross table을 만들기 어려워서 끙차끙차 만들어 보겠습니다.
df_v40 = df_version_40['retention_1'].value_counts()
df_v40
False 23584 True 10069 Name: retention_1, dtype: int64
df_v30 = df_version_30['retention_1'].value_counts()
df_v30
False 23079 True 10292 Name: retention_1, dtype: int64
df_cross_retention_1 = pd.DataFrame([df_v30, df_v40]).set_axis(['v30', 'v40'])
df_cross_retention_1
False | True | |
---|---|---|
v30 | 23079 | 10292 |
v40 | 23584 | 10069 |
이런 식의 테이블이 되어야 하겠습니다. 이렇게 해서 version_30과 version_40의 동질성을 카이스퀘어로 검정하면 되겠는데 말이죠. 자, 동질성 검정 (카이스퀘어)를 실행!
* 이것은 "독립성(연관성), 동질성 검정의 차이와 그들의 정체 - χ² 카이스퀘어 검정" 편을 보면 이해에 도움이 될 것입니다.
from scipy.stats import chi2_contingency
chi2, p, dof, expected = chi2_contingency([df_v30, df_v40], correction=False)
msg = 'Statistic: {}\np value: {}\ndof: {}'
print(msg.format(chi2, p, dof))
print(expected)
Statistic: 6.721234772740104 p value: 0.009527164450077289 dof: 1 [[23233.33392516 10137.66607484] [23429.66607484 10223.33392516]]
어랏? version_30과 version_40이 retention_1에 대해서 동질하지 않군요? 에헴?
그럼 retention_7에 대해서 두 버전의 동질성을 확인하면,
df_v30_7 = df_version_30['retention_7'].value_counts()
df_v30_7
False 31194 True 2177 Name: retention_7, dtype: int64
df_v40_7 = df_version_40['retention_7'].value_counts()
df_v40_7
False 31688 True 1965 Name: retention_7, dtype: int64
df_cross_retention_7 = pd.DataFrame([df_v30_7, df_v40_7]).set_axis(['v30', 'v40'])
df_cross_retention_7
False | True | |
---|---|---|
v30 | 31194 | 2177 |
v40 | 31688 | 1965 |
chi2, p, dof, expected = chi2_contingency([df_v30_7, df_v40_7], correction=False)
msg = 'Statistic: {}\np value: {}\ndof: {}'
print(msg.format(chi2, p, dof))
print(expected)
Statistic: 13.545392354269547 p value: 0.0002328623913436502 dof: 1 [[31308.71362497 2062.28637503] [31573.28637503 2079.71362497]]
아니 이거 두 버전의 retention_7도 동질하지는 않군요. 자 얼마나 차이가 있는지 볼까요? 이걸 비교하기 위해서는 비율로 된 table을 만들어서 볼 필요가 있는데, 한번 만들어 보죠 머.
display(df_cross_retention_1), display(df_cross_retention_7)
False | True | |
---|---|---|
v30 | 23079 | 10292 |
v40 | 23584 | 10069 |
False | True | |
---|---|---|
v30 | 31194 | 2177 |
v40 | 31688 | 1965 |
(None, None)
흠~ 잘 만들어졌군요. %로 normalize를 해 보겠습니다요.
total = df_cross_retention_1.sum().sum()
df_cross_retention_1 = df_cross_retention_1/total*100
display(df_cross_retention_1)
total = df_cross_retention_7.sum().sum()
df_cross_retention_7 = df_cross_retention_7/total*100
display(df_cross_retention_7)
False | True | |
---|---|---|
v30 | 34.433934 | 15.355693 |
v40 | 35.187396 | 15.022977 |
False | True | |
---|---|---|
v30 | 46.541537 | 3.248090 |
v40 | 47.278587 | 2.931786 |
가만히 들여다 보면, 30버전이 40버전에 비해 1일 7일 Rtention이 모두 높습니다. 그렇다면 30번째 스테이지에서 Lock을 걸면 되겠군?! 이라는 생각에 미치게 됩니다.
결국에는 30 레벨에서 lock을 거는 버전이 더 1일, 7일 retention이 모두 높다는 이야기인데 말이죠. 실제로 통계적으로 차이가 나는지 proportional z Test를 시도! 해 보겠습니다.
차이 검정 proportional z Test
자, 실제로 v30과 v40에서 1 day retention, 7 day retention이 차이가 나는지를 검정해 보겠습니다.
먼저 1day retention!
Null : 30 retention 1day >= 40 retention 1day
Alternative : 30 retention 1day < 40 retention 1day
로 설정해서 검정해 보시죵
import numpy as np
from statsmodels.stats.proportion import proportions_ztest
count_a = len(df_version_30[df_version_30['retention_1']==True])
nob_a = len(df_version_30.index)
count_b = len(df_version_40[df_version_40['retention_1']==True])
nob_b = len(df_version_40.index)
count = np.array([count_a, count_b])
nobs = np.array([nob_a, nob_b])
stat, pval = proportions_ztest(count, nobs, alternative="smaller")
print(count)
print(nobs)
print("z_stat statsmodels : %f" %(stat)) # 라이브러리 이용
print("p_val statsmodels : %f " %(pval)) # 라이브러리 이용
[10292 10069] [33371 33653] z_stat statsmodels : 2.592534 p_val statsmodels : 0.995236
어허, 1 day retention에 관련해서 v30의 0번 플레이한 사용자수가 이 v40에 비해 크거나 같다는 Null Hypothesis를 기각할 수가 없군요. 차이가 있습니다? 그러니까 1 day retention도 v30이 1day retention에서 실제로 더 괜찮군요. 결과는 알겠는데 왜죠.
그럼 두번째로 7day retention!
가설은 똑같겠죠!
Null : 30 retention 7day >= 40 retention 7day
Alternative : 30 retention 7day < 40 retention 7day
로 설정해서 검정해 보시죵
from statsmodels.stats.proportion import proportions_ztest
count_a = len(df_version_30[df_version_30['retention_7']==True])
nob_a = len(df_version_30.index)
count_b = len(df_version_40[df_version_40['retention_7']==True])
nob_b = len(df_version_40.index)
count = np.array([count_a, count_b])
nobs = np.array([nob_a, nob_b])
stat, pval = proportions_ztest(count, nobs, alternative="smaller")
print(count)
print(nobs)
print("z_stat statsmodels : %f" %(stat)) # 라이브러리 이용
print("p_val statsmodels : %f " %(pval)) # 라이브러리 이용
[2177 1965] [33371 33653] z_stat statsmodels : 3.680407 p_val statsmodels : 0.999884
어허허허, 7 day retention에 관련해서도 v30 이 v40에 비해 크거나 같다는 Null Hypothesis를 기각할 수가 없군요. 확률적으로 차이가 있습니다? 그러니까 7 day retention도 v30이 실제로 더 괜찮군요.
여기에서 결론은
30버전 즉, 30레벨에서 Lock을 거는 편이 1일, 7일 retention을 높일 수가 있을거라 잠정 결론입니다. 이미 아쉬움을 이야기 했지만 몇번째 플레이에 30레벨이 되었는지에 대한 정보가 있었다면 더 명확한 결론을 이끌어 낼 수 있었을 것이라 생각합니다.
이 순간 관심이 가는 것이 하나 있는데 30버전과 40버전 사이에 Play를 아예 하지 않은 사용자들이 차이가 나는가 입니다. 똑같이 proportional z Testing을 해 보시죠.
플레이를 아예 안한 사용자의 비율이 다른가? (Play 0회)
0번 플레이한 사용자 수에 대해서 가설을 설정해 보면,
Null Hypothesis : 30 noplay <= 40 noplay
Alternative Hypothesis : 30 noplay > 40 noplay
이런 식으로 단측검정 가설을 설정할 수 있겠습니다.
df_raw = pd.read_csv("cookie_cats.csv")
df_v30 = df_raw[df_raw['version']=='gate_30']
df_v40 = df_raw[df_raw['version']=='gate_40']
count_30 = len(df_v30[df_v30['sum_gamerounds']==0])
count_40 = len(df_v40[df_v40['sum_gamerounds']==0])
nob_30 = len(df_v30)
nob_40 = len(df_v40)
count = np.array([count_30, count_40])
nobs = np.array([nob_30, nob_40])
stat, pval = proportions_ztest(count, nobs, alternative="larger")
print(count)
print(nobs)
print("z_stat statsmodels : %f" %(stat)) # 라이브러리 이용
print("p_val statsmodels : %f " %(pval)) # 라이브러리 이용
[1937 2057] [44700 45489] z_stat statsmodels : -1.376798 p_val statsmodels : 0.915713
이 결과를 보면 30버전이 더 0번 플레이한 사용자수가 더 적다는 Null Hypothesis를 기각할 수 없겠습니다. 그렇다면 어쨌든 30버전이 처음부터 지운 사용자수도 적군요. 하아. 왜 40 lock version을 시장에 내 보낸걸까요..?
플레이를 많이 한 충성 유저들만 따로 자세히 보는 이야기. (50회 초과)
df_raw_outlier = pd.read_csv("cookie_cats.csv")
df_raw_outlier = df_raw_outlier[df_raw_outlier['sum_gamerounds']>50]
df_raw_outlier.describe()
userid | sum_gamerounds | |
---|---|---|
count | 2.281200e+04 | 22812.000000 |
mean | 5.005686e+06 | 164.157154 |
std | 2.880994e+06 | 364.728302 |
min | 3.770000e+02 | 51.000000 |
25% | 2.521051e+06 | 71.000000 |
50% | 4.989110e+06 | 109.000000 |
75% | 7.490854e+06 | 190.000000 |
max | 9.999768e+06 | 49854.000000 |
이렇게 보니 평균은 164회 플레이인데, 최대가 49854회입니다. 아까 아~주 많이 플레이한 미친 넘이 있었죠?
이게 누군지 한번 볼까요?
df_raw_outlier[df_raw_outlier['sum_gamerounds']==49854]
userid | version | sum_gamerounds | retention_1 | retention_7 | |
---|---|---|---|---|---|
57702 | 6390605 | gate_30 | 49854 | False | True |
30 레벨에 lock을 건 버전 사용자면서 6390605번 사용자입니다. 특이점은 설치 후 다음 날은 플레이를 안했군요. 정말 특이하군요.
그러면, 이런 식의 미친 넘들이 gate_30에 더 많은지, gate_40에 더 많은지 궁금해 지는군요?
import matplotlib.pyplot as plt
df_raw_outlier[df_raw_outlier['version']=='gate_40']['sum_gamerounds'].plot.box(title='gate_40')
plt.show()
df_raw_outlier[df_raw_outlier['version']=='gate_30']['sum_gamerounds'].plot.box(title='gate_30')
plt.show()
이렇게 비교해 보니, gate_30 버전에 아~주 미친 사용자 때문에 비교가 어렵군요. 그러면 이런 미친 사용자는 제거하고 비교해 보겠습니다. 이런 넘은 회사에서 잘해줘야 하는지, 아니면 무시해야 할런지 그것도 결정해야할 문제인 것 같군요. 이 놈도 30 version을 쓰는 넘이군요. ㅠㅠ?
import matplotlib.pyplot as plt
df_raw_outlier = df_raw_outlier[df_raw_outlier['userid']!=6390605]
df_raw_outlier[df_raw_outlier['version']=='gate_40']['sum_gamerounds'].plot.box(title='gate_40')
plt.show()
df_raw_outlier[df_raw_outlier['version']=='gate_30']['sum_gamerounds'].plot.box(title='gate_30')
plt.show()
자주 봐야 하는 데이터니까 30과 40을 따로 dataframe으로 만들어 놓죠.
df_raw_outlier_gate_40 = df_raw_outlier[df_raw_outlier['version']=='gate_40']
df_raw_outlier_gate_30 = df_raw_outlier[df_raw_outlier['version']=='gate_30']
print(df_raw_outlier_gate_40['sum_gamerounds'].describe())
print(df_raw_outlier_gate_30['sum_gamerounds'].describe())
count 11676.000000 mean 160.709404 std 158.045849 min 51.000000 25% 71.000000 50% 107.000000 75% 186.000000 max 2640.000000 Name: sum_gamerounds, dtype: float64 count 11135.000000 mean 163.309924 std 156.765199 min 51.000000 25% 71.000000 50% 110.000000 75% 192.000000 max 2961.000000 Name: sum_gamerounds, dtype: float64
이렇게 보니까 많이 한 플레이한 수도 버전에 따라서 얼~추~ 비~슷 합니다.
이제까지 30버전이 더 낫다는 잠정 결론들이 있었으니까, 30버전에 대해서만 해보죠.
이 지경까지 오니, 알아보고 싶은 것은 미친 넘들의 1day retention과 7day retention의 상관관계를 알고 싶군요?
이럴 떄는 어떻게해요? 또 contingency table로 만들면 좋겠군요! 그러니까 어떤 식의 contingency table이냐면,
30버전이 우리의 관심 버전이니까, 30버전에서 각각에서 1 day retention과 7 day retention이 독립인가?와 Cramer V 상관계수를 한번 보시죠. 이 경우에는 곧바로 cross table을 만들 수 있습니다.
"교차분석의 완성 = Cross Tabulation + χ² Testing + Cramer V 연관도" 편을 참고하면 더 이해하기 쉽겠습니다.
import numpy as np
from scipy.stats.contingency import association
from scipy.stats import chi2_contingency
# contingecy table
df_cross_table = pd.crosstab(df_raw_outlier_gate_30['retention_1'], df_raw_outlier_gate_30['retention_7'])
# chi square testing - 독립인지?
chi2 = chi2_contingency(df_cross_table, correction=False)
# cramer's V 얼마나 연관되어 있는지?
corr_cramer = association(df_cross_table, method="cramer")
print("tabulation table")
display(df_cross_table)
print("chi2_test result")
display(chi2)
print("Cramer V")
display(corr_cramer)
tabulation table
retention_7 | False | True |
---|---|---|
retention_1 | ||
False | 740 | 802 |
True | 4125 | 5468 |
chi2_test result
(13.442997689110339, 0.00024592235380417214, 1, array([[ 673.71621015, 868.28378985], [4191.28378985, 5401.71621015]]))
Cramer V
0.03474585086872759
30버전에서의 미친 넘들을 보니 retention이 1일과 7일에서 독립이 아닙니다? 어허, 그렇군요. 영향을 서로에게 미칠 가능성이 큽니다.
그런데 막상 연관도를 측정해 보니, 1 day retention과 7 day retention끼리의 연관성은 또 그렇게 크지는 않군요. 0.03474585086872759. 그러니까, 이것은 1 day retention과 7 day retention은 서로 영향을 미치긴 하지만, 그렇게 까지 크진 않다 정도로 결론 맺을 수 있겠습니다.
결론/ 주장
⓵ Lock을 걸거면 30 레벨에서 거는 편이 낫다. 그러면 retention에 도움이 된다.
- 30레벨 버전이 Play횟수도 많고, Retention도 40레벨 버전에 비해서 더 좋다는 통계적 결론.
⓶ 플레이를 엄청 많이 한 미친 넘이 1명 있는데, 회사에서 더 잘해줘야 할지 말아야 할지 결정을 해야 한다.
⓷ 미친 넘들(이라고 쓰고 충성고객들이라 읽는다)에 대한 1일차 retention과 7일차 retention은 서로 독립이 아니다, 그러나 의외로 그 연관도는 상당히 낮다. 주어진 데이터로는 미친 넘들에게도 1일 retention이 7일 retention과 연관은 있지만 매우 작다. 하지만, 7일 retention을 좋게 한다고 해서 더 많은 사용자가 남을 것이라는 증거는 없다.
⓸ 왠지는 모르겠지만 설치 후 게임을 삭제하는 비율도 40버전이 통계적으로 크다고 볼 수 있겠습니다.
정도가 되지 않을까 생각합니다.
의식의 흐름대로 분석을 하다보니 딱 떨어지는 맛이 없긴 한데, 조금 더 다채로운 데이터가 특히 30버전에서 사용자별 30버전을 달성한 날수와 게임 횟수가 있었다면 retention에 대한 Insight를 조금 더 발굴할 수 있지 않았을까 하는 아쉬움이 있습니다.
도대체 왜 40버전을 테스트 한 걸까요? 결과로만 보면 Lock이 문제가 아니라 여러가지 면에서 다른 버전일 가능성이 커 보입니다.
이 분석은 최초에 차이들을 다른 식으로 분석했다가, 통계적 차이 분석을 proportinal z Test와 카이제곱 분석으로 다시 하여 작성한 분석입니다. ㅠㅠ
댓글