사쿠의 데이터 블로그

[R code] 유저 플레이 타임(Play time) 군집화 본문

게임 데이터 분석

[R code] 유저 플레이 타임(Play time) 군집화

토스트먹어 2020. 3. 26. 23:47

제목은 플레이 타임 군집화라고 적었지만, 시계열 데이터에 군집화를 적용할 수 있는 방법론을 소개해드립니다.

먼저, 내용의 출처는 아래 논문이고 코드 부분은 제가 작성했습니다. 본문 내용 중 궁금한 내용이 있다면 언제든지 질문 부탁드립니다.

 

Discovering Playing Patterns: Time Series Clustering of Free-To-Play Game Data

The classification of time series data is a challenge common to all data-driven fields. However, there is no agreement about which are the most efficient techniques to group unlabeled time-ordered data. This is because a successful classification of time s

arxiv.org


글의 구성은 소개 드리는 논문과 작성한 코드 리뷰를 해보려 합니다.

  • 개요 (Abstract)
  • 소개 (Introduction)
  • 군집 방법 소개
  • 결과 확인
  • 코드 확인

 

개요(Abstract)

Label이 없는 시계열 데이터를 군집화 하는 가장 효율적인 방법론에 대한 합의는 이루어지지 않았습니다. 왜냐하면 시계열 패턴의 분류는 분석가의 목표와 관심 영역(domain)에 따라 다르기 때문입니다.

예를들어, 

  • 일자별 기상 데이터 군집

  • 시간별 전력 수요량 군집

등 다양한 분야에서 시계열 데이터를 군집화하려 하지만 일관된 방법론이 존재하지 않습니다. 가령, R의 dtwclust 패키지가 존재하지만, 본 논문의 방법보다는 결과가 좋지 않음을 확인했습니다.

본 논문에 사용된 데이터는 F2P(Free to Play)로, 유저 행동 시계열 데이터에 중점을 두어 정확한 군집화 방법을 평가 했습니다. 저자들은 실험을 통해 군집 결과를 검증하고 최적의 군집수를 결정하는 방법을 직관적인 시각화를 통해 제공했습니다. 

 

소개(Introduction)

본 논문은 행동(behavior) 데이터 군집화를 할 수 있다면, 유저를 더 잘 이해할 수 있다고 생각해 작성했다고 합니다. 기존에도 유저들의 군집화는 종종 있었지만, 시계열 데이터를 다루지는 않았다고 합니다. 

(본 논문이 최초?!)

분석에 사용되는 데이터는 "Age of Ishtaria"의 일별 플레이 시간(Time played per day) 데이터 입니다. 즉, 유저들의 일별 플레이 타임 변화를 군집화 할 수 있다면, 

  1. 꾸준히 접속하는 유저 군집을 발견할 수 있음

  2. 이벤트 혹은 업데이트에 반응하는 군집을 찾을 수 있음 (좋은 쪽과 나쁜쪽 둘다)

  3. 1번과 반대로 플레이 시간이 줄어드는 군집을 발견할 수 있음 

만약, 3번 군집을 발견할 수 있다면 게임 이탈방지에 도움이 될 수 있습니다. 

 

군집 방법 소개

  • Trend
  • COR
  • Hierarchical cluster

먼저, 시계열 데이터는 Trend(추세) + Seasonal (계절성) +random(잔차)로 분해할 수 있습니다. 논문에서 Trend는 계산의 용이성과 게임의 업데이트 주기에 맞춰 7일 이동 평균(7-MA)을 이용했습니다. 이동 평균을 사용하는 이유는 유저의 관심정도가 반영되어 있는 정보이며, raw데이터에 비해 변동이 적어 분석에 용이 합니다.

그래서, 가장 먼저 해야할 일은 유저마다 이동 평균을 구합니다!

시계열 데이터의 분해(decompose)

 

다음으로, 유저마다 Trend 상관계수(COR)를 계산합니다. 상관계수를 구하는 것에 의아해 하실 수 있습니다. 아래 간단한 예시를 통해 상관계수와 유클리디안 거리를 비교해 보겠습니다.

유저 플레이 타임 데이터 군집 예시

유저 A, 유저 B, 유저 C 를 두고 설명을 해보려 합니다. 유저별 플레이 스타일은 다음과 같습니다.

  • 유저 A: 매일 플레이 타임이 증가
  • 유저 B: 매일 꾸준한 플레이를 하는 타입 
  • 유저 C: A보다는 플레이 타임이 적지만 그래도 플레이 타임이 상승세

먼저, 상관계수를 보면 COR(A, B) = 0.86으로 가장 높습니다. 둘은 플레이 타임이 상승세라는 점도 비슷합니다. 다음으로 유클리드 거리로는 B,C가 가장 가깝지만, 둘 사이의 거리만 작을 뿐, 추세가 이어진다면 B는 곧 A와 같게 될 것입니다.

이 때문에 Trend를 구한 뒤, COR를 구해 군집화를 시행하려 합니다.

 

마지막으로 계층 군집(Hierarchical cluster)을 ward연결법으로 시행하면 완성입니다.  이유는 다른 연결법 보다 분산(variance)이 가장 적고 노이즈나, 이상치에 덜 민감하기 때문입니다. 와드 연결에 대한 자세한 설명은 다음 링크를 참조 부탁드립니다. 와드연결

이제, 결과를 확인 해보겠습니다.

 

결과확인

앞서 말씀 드린대로, 분석에 사용한 데이터는 "Age of Ishtaria"의 3주동안의 일별 플레이 시간 데이터 입니다. 군집은 8개로 나누었고 각 군집별 특성은 아래와 같습니다. 

class 3(16.1%)은 3주차에 좋은 플레이 타임이 증가하는 반응을 보였습니다. Table2를 참조하면 평균 레벨도 가장 높은 것을 알 수 있습니다. 

반대로, class 4(6.5%)는 2주차때 플레이 타임이 높았지만, 3주차로 가면서 줄어드는 모습을 모입니다. Table2를 참조하면 평균 레벨이 낮은것이 특징입니다.

사실, Week3에는 이벤트가 있었다고 합니다. 해당 분석을 통해 이벤트에 반응하는 군집을 밝혀낼 수 있었습니다. 이번 이벤트는 저레벨 유저보다는 고레벨 유저들에게 효과가 있었습니다.

마지막으로 Table3은 군집별 이탈율(Churn)을 살펴봤습니다. 3주차때 반응이 좋았던 3, 7번 군집은 이후에도 이탈률이 낮았지만 반응이 좋지 않았던 4, 8군집은 이탈률도 높은것이 특징입니다.

 

코드 확인 - 실행환경(window)

주요 라이브러리 정보

library(ggplot2) # for vis
library(fastcluster) # for clustering
library(RcppArmadillo) # Use RcppXPtrUtils for simple usage of C++
library(RcppXPtrUtils)   # compile user-defined function

파일 구성

corFuncPtr.R

# 병렬처리 
# http://arma.sourceforge.net/docs.html

# RcppArmadillo is used as dependency
library(RcppArmadillo)
 # Use RcppXPtrUtils for simple usage of C++ external pointers
library(RcppXPtrUtils)
# compile user-defined function and return pointer (RcppArmadillo is used as dependency)

corFuncPtr <- cppXPtr("double customDist(const arma::mat &A, const arma::mat &B) {
                                 return sqrt( 2 * ( 1 - arma::accu(arma::cor(A , B)) )); }",
                            depends = c("RcppArmadillo"))

 

 

time_series_cluster.R

# 데이터 읽기
 # nid : 유저 아이디
 # rn  : 구분자 번호 (10분단위, 1일단위 등)
 # pt  : rn에 맞는 수치

source("corFuncPtr.R") # 자동으로 패키지 설치 안내문으뜨면 설치하면 된다.
library(parallelDist) # https://www.rdocumentation.org/packages/parallelDist/versions/0.1.1/topics/parDist
library(data.table) # read CSV file
library(dplyr)
library(reshape)
library(forecast) # MA 계산
library(ggplot2) # for vis
library(RColorBrewer) # for vis
library(gridExtra) # for vis


data<- fread('데이터.csv', integer64 = 'numeric')
colnames(data)<- c('nid', 'rn', 'pt')

# Hyper parameter
max_rn <- max(data$rn)
MA <- 7


# COR matrix 작성
 # 유저간 trend 데이터를 담아두는 공간
nid_list<- as.character(unique(data$nid))
COR<- matrix(0, nrow = length(nid_list), ncol = max_rn - MA +1 )
rownames(COR)<- nid_list

# trend 계산
 # nid(유저)별 모든 seq데이터를 생성해 놨기 때문에 아래와 같이 수행 가능
 # max_rn 의 크기만큼 nid별 인덱스를 생성
start_list<- seq(1, nrow(data), by = max_rn)
end_list<- seq(1+max_rn-1, nrow(data)+max_rn-1, by = max_rn)

temp_list<- vector(mode ='list', length = length(nid_list))
for(i in 1:length(nid_list)){
  temp_list[[i]]<- start_list[i] : end_list[i]  
}
for(i in 1:length(nid_list)){
  qq <- forecast::ma(data[temp_list[[i]],'pt'], order = MA)
  qq <- qq[!is.na(qq)]  
  COR[i,]<-qq
}




# COR + trend 구하기
d<- parDist(COR, method="custom", func = corFuncPtr)
d[is.na(d)]<-0

# 군집
  # Hierarchical clustering using Ward Linkage
hc1 <- fastcluster::hclust(d, method = "ward.D" )

# Plot the obtained dendrogram
 # 데이터가 많으면 굳이 안그릴 것을 추천
# plot(hc1, cex = 0.01, hang = -1, main= 'User play Time cluster', label = FALSE)
# rect.hclust(hc1, k = 6, border = 2:8)


# Cut tree into 3 groups
sub_grp <- cutree(hc1, k = 6)

# Number of members in each cluster
table(sub_grp)

 

중간에 C++ 라이브러리를 이용했습니다. 이유는 R 내장 함수를 이용하여 계산하면, 실 서비스에 부담이 갈 정도로 계산이 느리기 때문입니다. 사용방법은 간단하니, 두려울 필요는 없습니다!  

 

 

블로그 포스팅이 익숙치 않아 내용 전달이 잘 됐을지 모르겠습니다. 

혹, 부족한 내용이 있다면 말씀 부탁드립니다.

감사합니다.

'게임 데이터 분석' 카테고리의 다른 글

유저 이탈 기준 정하기  (0) 2020.11.20
LTV 예측하기  (2) 2020.05.24
Stacking 혹은 Meta모델링 적용하기  (0) 2020.04.26
게임회사 지표 활용사례 (매출 - ARPPU)  (0) 2019.10.26
게임 지표 용어 정리  (0) 2019.10.16