ASAC 6기/EDA 프로젝트

[EDA 프로젝트] 게임 스트리밍과 게임 판매량의 연관성 분석 1: 데이터 수집 및 전처리

helena1129 2024. 9. 16. 21:15

주제를 게임 스트리밍과 게임 판매량의 연관석 분석이라 설정했지만, 정확히 내가 하려는 분석의 방향성은

게임의 특성별(장르 등) 게임 스트리밍과 게임 판매량의 상관관계를 알아보는 것이었다.

예를 들어, RPG 장르가 비주얼 노벨 장르보다 게임 판매량에 스트리밍의 영향이 더 크다(홍보 효과가 크다)라는 것 등의 분석 결과가 나올 수 있도록 접근하는 것을 목표로 했다. 

 

주제를 정하고 나서 가장 품이 많이 드는 파트라 해도 과언이 아닌 데이터 수집 파트에 돌입했다.

주제 정하기나 분석은 이것저것 생각하느라 머리가 아파서 힘든 거지만 데이터 수집은 여러 사이트를 돌아다녀야 하다 보니 시간과 노력이 꽤 든다.

 

데이터 수집 및 전처리

주제를 기반으로 필요한 데이터는 다음과 같이 정리했다.

게임 정보 데이터 + 스트리밍 데이터 + 게임 판매량 데이터

 

1. 게임 데이터

1-1) 데이터 수집 준비(API)

 

게임의 특성에 따른 비교를 위해 게임 정보 데이터가 반드시 필요했다.

나는 대표적인 글로벌 게임 스트리밍 플랫폼인 트위치와 트위치가 운영하는 게임 데이터베이스 사이트인 IGDB의 API를 활용했다.

IGDB API: https://api-docs.igdb.com/#getting-started

Twitch API: https://dev.twitch.tv/docs/api/

 

Twitch API

Twitch API

dev.twitch.tv

 

트위치 API를 사용하기 위해서는 사용자 토큰이 필요하다.

토큰 등록을 위해서는 트위치 아이디를 만든 후 API 홈페이지 오른쪽 위의 Your Console에서 응용 프로그램을 등록해야 한다.

이름은 원하는 대로 짓고 OAuth 리디렉션 URL에 http://localhost를 넣어주고, 범주를 Other로 선택, 클라이언트 유형을 기밀로 설정한 후 등록하면 클라이언트 ID가 생성된다.

생성된 응용 프로그램에서 클라이언트 시크릿을 준비해 주면 된다.

 

파이썬으로 넘어와서 준비된 정보를 기반으로 토큰을 조회하면 된다.+ 클라이언트 아이디와 시크릿은 변하지 않으니 코드를 써 놓고 필요할 때마다 실행하면 편하다.

import requests
req = requests.post(f'https://id.twitch.tv/oauth2/token?client_id={client_id}&client_secret={client_secret}&grant_type=client_credentials')
print(req.text)

 

그러면 이러한 형식으로 출력이 된다.

{"access_token":"유저 토큰","expires_in":만료 기간,"token_type":"bearer"}

 

액세스 토큰은 출력할 때마다 변한다.

API 요청 시 필요한 헤더를 이렇게 등록해 놓으면 트위치 API 요청을 위한 준비는 끝났다.

headers = {"Client-Id" : client_id,
           "Authorization" : f"Bearer {access_token}"}

 

1-2) GetTopGames(Twitch) 

https://dev.twitch.tv/docs/api/reference/#get-top-games

 

Reference

Twitch Developer tools and services to integrate Twitch into your development or create interactive experience on twitch.tv.

dev.twitch.tv

분석을 위해 어느 정도 스트리밍이 활성화된 게임이 좋겠다고 생각해서 기준이 될 게임 목록을 GetTopGames에서 가져왔다.

 

참고로 트위치와 같은 외국 사이트 API에서는 리퀘스트 예시를 다음과 같이 CURL 형식으로 주는 경우가 있다.

curl -X GET 'https://api.twitch.tv/helix/games/top' \
-H 'Authorization: Bearer cfabdegwdoklmawdzdo98xt2fo512y' \
-H 'Client-Id: uo6dggojyb8d6soh92zknwmi5ej1q2'

 

이를 CURL Converter 사이트에 입력하면 원하는 프로그래밍 언어로 리퀘스트 예시를 들어준다.

나는 다음과 같이 최대 만 개 정도의 게임을 뽑으려고 했고, 돌려보니 약 3000개를 넘겼을 때 출력이 중단되었다.

import requests
import time

top_games = []

params = {
  'first':'100',
  'after':''
}

while len(top_games) <= 9900:

  req = requests.get('https://api.twitch.tv/helix/games/top', params=params, headers=headers)
  games = req.json()

  top_games.extend(games['data'])

  print(f"{len(games['data'])}개의 게임을 수집했습니다. 현재까지 총 {len(top_games)}개의 게임을 수집했습니다.")

  params['after'] = games['pagination']['cursor']

  time.sleep(1)

 

이런 식으로 데이터프레임을 생성했다. IGDB API와의 연계를 위해 igdb_id가 필요하다.

id name igdb_id
32982 Grand Theft Auto V 1020
516575 VALORANT 126459
21779 League of Legends 115
770229477 60 Seconds! Reatomized 120560
29452 Virtual Casino 45517
... ... ...
697266534 Rock Life: The Rock Simulator 228299
516747 Weed Farmer Simulator 128479
19343 Dissidia Final Fantasy 391
188824012 Internet Cafe Simulator 2 160171
495563 Project CARS 2 26709

3047 rows × 3 columns

 

1-3) Games(IGDB)

https://api-docs.igdb.com/#game

장르를 비롯한 게임의 특성이 필요했기에 IGDB API의 Games를 활용했다. Games API에는 여러 필드들이 존재해서 문서를 충분히 읽고 필요한 데이터를 가져오는 과정이 필요했다. 여러 시행착오를 겪은 후 내가 가져온 필드는 다음과 같다.

- genres: 장르. 장르 API와 연계(추후 업데이트).

- created_at: IGDB 데이터 생성일자. 사실 first_release_date가 발매일 필드지만 오류가 많은 것 같아 대체했다. Unix Time Stamp 형태로 데이터가 생성되는데, 마찬가지로 Unix Time Stamp를 변환해 주는 사이트에서 변환해도 되고, 파이썬 라이브러리를 활용해도 된다.

- multiplayer_modes: 멀티플레이 모드에 관한 데이터. 멀티플레이 API와 연계 가능하지만 나는 이진수로 바꿔 멀티플레이 모드를 지원하느냐 마느냐의 여부만 보기로 했다.

- player_perspectives: 플레이어 시점. 플레이어 시점 API와 연계(추후 업데이트).

이 데이터들을 아까 수집한 트위치 데이터프레임에 IGDB_ID를 기준으로 이어 주기로 했다.

 

raw['genres'] = ''
raw['created_at'] = ''
raw['multiplayer_modes'] = ''
raw['player_perspectives'] = ''

raw_copy = raw.copy()

url = 'https://api.igdb.com/v4/games'

for i in range(len(raw_copy)):
  id = raw_copy.iloc[i]['igdb_id']

  query = f'''fields id, name, genres, created_at, multiplayer_modes, player_perspectives;
            where id = {id};'''

  req = requests.post(url, headers=headers, data=query)
  temp = req.json()

  if 'genres' in temp[0].keys():
    raw_copy.iloc[i]['genres'] = temp[0]['genres']
  if 'created_at' in temp[0].keys():
    raw_copy.iloc[i]['created_at'] = temp[0]['created_at']
  if 'multiplayer_modes' in temp[0].keys():
    raw_copy.iloc[i]['multiplayer_modes'] = temp[0]['multiplayer_modes']
  if 'player_perspectives' in temp[0].keys():
    raw_copy.iloc[i]['player_perspectives'] = temp[0]['player_perspectives']

 

id name igdb_id genres created_at multiplayer_modes player_perspectives
32982 Grand Theft Auto V 1020 [5, 10, 31] 1326127365 1 [1, 2]
516575 VALORANT 126459 [5, 24] 1575122198 1 [1]
21779 League of Legends 115 [12, 15, 36] 1300101163 1 [3]
770229477 60 Seconds! Reatomized 120560 [12, 13, 15, 31, 32] 1562778088 1 [2]
29452 Virtual Casino 45517 [35] 1499509233 0  
... ... ... ... ... ... ...
697266534 Rock Life: The Rock Simulator 228299 [13, 32] 1670030423 0  
516747 Weed Farmer Simulator 128479 [13, 32] 1579284459 0  
19343 Dissidia Final Fantasy 391 [4, 12, 25, 31] 1300443348 0 [2]
188824012 Internet Cafe Simulator 2 160171 [13, 15, 31, 32] 1627906912 0 [1]
495563 Project CARS 2 26709 [10, 13] 1483751393 1 [1, 2, 7]

3047 rows × 7 columns

 

1-4) External Game(IGDB)

https://api-docs.igdb.com/#external-game

스팀 판매량과의 연계를 위해 External Game API에서 스팀 아이디를 조회하기로 했다.

External Game API에서 카테고리가 1인 부분이 스팀 아이디이다. 스팀 아이디가 존재하지 않는 데이터는 수집하지 않기로 했다.

raw_copy['steam_id'] = ''

raw_copy2 = raw_copy.copy()

url = 'https://api.igdb.com/v4/external_games'

for i in range(len(raw_copy2)):
  id = raw_copy2.iloc[i]['igdb_id']

  query = f'fields *; where game = {id} & category = 1;'

  req = requests.post(url, headers=headers, data=query)
  temp = req.json()

  if len(temp) != 0:
    raw_copy2.loc[raw_copy2['igdb_id']== id, 'steam_id'] = temp[0]['uid']
  else:
    raw_copy2.loc[raw_copy2['igdb_id']== id, 'steam_id'] = ''

 

id name igdb_id genres created_at multiplayer_modes player_perspectives steam_id
32982 Grand Theft Auto V 1020 [5, 10, 31] 1326127365 1 [1, 2] 271590
770229477 60 Seconds! Reatomized 120560 [12, 13, 15, 31, 32] 1562778088 1 [2] 1012880
1808565595 The Casting of Frank Stone 279635 [31] 1702000534 0 [2] 2223840
2068583461 NBA 2K25 308034 [13, 14] 1720626296 0 [2] 2878980
491487 Dead by Daylight 18866 [15] 1461403829 1 [1, 2] 381210

 

1-5) 전처리

이후 진행한 전처리는 다음과 같다.

- 중복 제거

- 플레이어 시점 결측치 채우기

- 장르 없는 행 드롭

- Genre API를 통해 장르 이름 붙이기

- 플레이어 시점 API를 통해 플레이어 시점 붙이기

- Unix Time Stamp 변환

 

그리하여 다음과 같은 게임 정보 데이터가 최종으로 완성되었다!

id igdb_id steam_id name genres created_at multiplayer_modes player_perspectives
32982 1020 271590 Grand Theft Auto V ['Shooter', 'Racing', 'Adventure'] 2012-01-09 1 ['First person', 'Third person']
770229477 120560 1012880 60 Seconds! Reatomized ['Role-playing (RPG)', 'Simulator', 'Strategy'... 2019-07-10 1 ['Third person']
1808565595 279635 2223840 The Casting of Frank Stone ['Adventure'] 2023-12-08 0 ['Third person']
2068583461 308034 2878980 NBA 2K25 ['Simulator', 'Sport'] 2024-07-10 0 ['Third person']
491487 18866 381210 Dead by Daylight ['Strategy'] 2016-04-23 1 ['First person', 'Third person']
... ... ... ... ... ... ... ...
510204 61616 704850 Thief Simulator ['Simulator', 'Strategy', 'Adventure', 'Indie'] 2017-09-08 0 ['First person']
68028 1593 291650 Pillars of Eternity ['Role-playing (RPG)', 'Strategy', 'Adventure'... 2012-10-14 0 ['Bird view / Isometric']
517515 131760 1256670 Library of Ruina ['Role-playing (RPG)', 'Simulator', 'Strategy'... 2020-03-05 0 ['Side view']
1465660616 277337 2565260 NOTICE ['Role-playing (RPG)', 'Indie'] 2023-11-20 0 []
188824012 160171 1563180 Internet Cafe Simulator 2 ['Simulator', 'Strategy', 'Adventure', 'Indie'] 2021-08-02 0 ['First person']

1611 rows × 8 columns

 

다른 전처리는 데이터 분석 과정에서 추가로 진행할 예정이다.

++ DLC 데이터 수집도 진행했지만, 데이터가 존재하는 게임보다 존재하지 않는 게임이 훨씬 많아 아예 DLC 출시 및 업데이트 영향을 배제하는 방향으로 분석하기로 했다.

 

2. 스트리밍 데이터

내가 필요한 데이터는 게임의 트위치 기간별 누적 스트림 정보였고, 처음엔 일주일 단위가 적합하다고 생각했다.

그러나 데이터 구하기가 여의치 않아서... 한 달 단위로 보기로 결정하였고, 데이터 수집은 셀레니움을 활용했다.

혹시 모를 잡음을 피하기 위해 데이터 수집 과정은 공개하지 않겠다.

 

3. 게임 판매량(리뷰) 데이터

데이터 수집 과정에 앞서 하나의 장벽에 마주했다.

바로 게임 판매량 데이터를 수집하기 어렵다는 점...!

게임 판매량 데이터를 제공하는 여러 사설 사이트가 존재하긴 했지만, 유료일뿐더러 판매량의 '추정치'만 제공했다.

아무래도 업계에서 예민한 데이터인 만큼 대중에게 공개를 꺼려하는 것처럼 보였다.

결국 나는 게임 판매량을 간접적으로 확인할 수 있는 데이터를 찾아야 했다.

 

스팀 주간 최고 인기 게임 차트

 

스팀에서도 다음과 같이 주간 차트를 제공하고 있긴 하지만, 저 '판매 수익'이라는 부분이 신경 쓰였다. 할인에 지나치게 영향을 많이 받을 것이라는 생각이 들었다. 그리고, 주간 100개의 게임밖에 제공하지 않았는데, 말이 100개지 소위 '고여 있는' 차트이기 때문에 이런저런 전처리를 거치면 유의미한 데이터를 찾기 힘들 것 같았다. 

 

그래서 활용하고자 한 것이 바로 리뷰 데이터이다.

LETHAL COMPANY의 스팀 리뷰 데이터

게임 발매 후 기간에 따라 스팀에서는 최대 한 달 단위로 리뷰 데이터를 제공하고 있다.

아무래도 직접 게임을 플레이한 후 리뷰를 작성하기 때문에 할인을 할 때 일단 사놓고 보는 경우도 어느 정도 배제가 가능하고, 게임이 많이 팔릴수록 리뷰의 개수도 늘어날 것이라고 판단했다. 최종적으로 게임 판매량을 알아보기 유의미한 정보라고 생각되어서 선택했다.

리뷰 데이터 수집은 웹페이지 크롤링을 통해 진행했다. 

게임 데이터 수집 때 찾아놓은 스팀 아이디를 활용하여 한 달 단위 총 리뷰 수 데이터를 수집했다. 


date recommendations_up recommendations_down steam_id
1427846400 18075 4672 271590
1430438400 6553 2126 271590
1433116800 5086 1619 271590
1435708800 3243 808 271590
1438387200 2576 565 271590

역시 Unix Time Stamp 형식으로 날짜 데이터가 제공되어 이를 변환하고, 추천과 비추천 데이터를 합쳐 총 리뷰수 데이터를 만들었다.

 

steam_id date recommendations_up recommendations_down recommendations_tot
20 2010-10-01 1 0 1
20 2010-11-01 10 0 10
20 2010-12-01 6 0 6
20 2011-01-01 6 0 6
20 2011-02-01 2 0 2
... ... ... ... ...
3010230 2024-09-01 157 112 269
3018910 2024-08-01 57 24 81
3018910 2024-09-01 3 0 3
3062970 2024-08-01 58 2 60
3076400 2024-08-01 27 8 35

90452 rows × 5 columns

 

야호! 데이터 수집이 모두 완료되었다.

다음은 본격적인 데이터 분석으로 돌아오겠다.