본문 바로가기
데이터분석/게임

LOL 그랜드마스터 도전기 : 프로선수와 나의 차이점은 무엇일까? - 레벨 편

by 바른 곰 2025. 9. 28.

주제 선정 배경

그랜드마스터 도전기 4번째 주제는 레벨 입니다.

 

롤에는 레벨 시스템이 존재합니다. 적 미니언이나 챔피언이 사망했을 때 근처에 있으면 경험치를 얻고, 일정 경험치를 초과하면 레벨업을 하게 됩니다. 레벨업 보상은 기본 능력치 증가와 스킬 포인트로, 아이템 외에도 자신을 강하게 만드는 또 다른 성장 수단입니다.

 

따라서 레벨은 인게임에서 내가 지속적으로 성장하고 있는지 확인할 수 있는 하나의 지표가 됩니다. 롤에서 성장이 멈추는 상황이 발생하면 승리 확률이 급격히 떨어지게 되기 때문에 이번에 프로 선수들과 비교했을 때, 제가 인게임에서 꾸준히 성장하는 모습을 보이고 있는지, 프로만큼 잘 성장하고 있는지 확인해 보려고 합니다.

 

나는 인게임에서 프로 선수들만큼 꾸준히 성장하고 있을까?
한번 확인해 보자

분석 아이디어

라이엇 API는 1분 간격으로 플레이어의 현재 레벨과, 현재까지 획득한 경험치량에 대한 로그 데이터를 제공한다. 이를 활용해 1분 간격으로 프로 선수와 나의 레벨 차이나, 경험치 획득량을 비교해 보려고 한다.


분석 과정 및 결론

1. 데이터 수집

먼저 라이엇 API를 통해 내 계정의 고유 ID인 puuid를 수집했다.

# puuid 불러오기

userNickname="바아른 곰" # 유저 닉네임 입력

tagLine="KR1" # 유저 태크 입력

encodedName = parse.quote(userNickname) # API 요청을 위한 유저 닉네임 인코딩

url = f"https://asia.api.riotgames.com/riot/account/v1/accounts/by-riot-id/{encodedName}/{tagLine}"

player_id = requests.get(url, headers=REQUEST_HEADERS).json()

puuid = player_id['puuid'] # 플레이어 고유 아이디

 

이렇게 수집한 나의 puuid는 다음과 같다.

 

그 다음 나의 puuid를 이용해서 내가 최근에 플레이한 경기들의 매치 ID를 수집했다.

# 매치ID 불러오기

match_ids = []

for i in range(0, 1):
  match_url = f"https://asia.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?start={i * 100}&count=100"
  match_response = requests.get(match_url, headers=REQUEST_HEADERS)
  if match_response.status_code == 200:
    match_ids.extend(match_response.json())
  elif match_response.status_code != 200:
    break

 

아래는 수집한 매치 ID다.

 

정확한 분석을 위해서는 추가 조건이 필요하다. 먼저 해당 게임이 소환사의 협곡 맵인지 판단해야 한다. 그 다음, 플레이 하는 포지션별로 와드 사용 방식이 달라지기 때문에 나의 주 포지션인 미드라인으로 플레이 한 게임인지 판단해야 한다.

 

이 정보는 게임에 대한 전반적인 정보를 개요를 제공하는 "매치 API"에서 얻을 수 있다. 아래 코드를 통해 협곡 맵이 아니거나, 미드 라인으로 플레이한 매치가 아닌 경우를 걸러냈다.

# 맵, 라인 확인
url = f"https://asia.api.riotgames.com/lol/match/v5/matches/{match_id}"
response = requests.get(url, headers=REQUEST_HEADERS)
time.sleep(1.3)
matchinfo = response.json()

# 맵ID 가 협곡인지 확인
if matchinfo['info']['mapId'] != 11:
  continue

# 라인을 미드로 플레이했는지 확인
participants = matchinfo['info']['participants']
is_mid = False
for participant in participants:
  if participant['puuid'] == puuid:
    if participant['individualPosition'] == 'MIDDLE' and participant['teamPosition'] == 'MIDDLE':
      is_mid = True
      break
      
if is_mid == False:
      continue

 

그 다음, 게임 중에 발생한 로그를 제공하는 "타임라인 API"를 활용해서 1분 간격으로 현재 내 레벨이 몇인지, 현재까지 획득한 총 경험치량이 얼마인지에 대한 데이터를 수집했다. 그렇게 수집한 데이터는 아래와 같다.

url = f"https://asia.api.riotgames.com/lol/match/v5/matches/{match_id}/timeline"  # 매치 로그에 대한 데이터 API
response = requests.get(url, headers=REQUEST_HEADERS)
time.sleep(1.3)
timeline = response.json()

# participantId 찾기
for user in timeline['info']['participants']:
  if user['puuid'] == puuid:
    participantId = user['participantId']

# 프레임별 미니언 처치 수 찾기
for frame in timeline['info']['frames']:
  event = {}
  event['matchId'] = match_id
  event['timestamp'] = frame['timestamp']
  event['xp'] = frame['participantFrames'][str(participantId)]['xp']
  event['level'] = frame['participantFrames'][str(participantId)]['level']
  event['유저'] = 유저[idx]
  event_list.append(event)

 

데이터는 해당 매치의 몇분 시점에 내가 총 얼마의 경험치를 획득했고, 현재 레벨은 몇인지를 나타낸다.

 

2. 데이터 전처리

먼저 밀리세컨드로 제공되는 timestamp(인게임 내 시간 변수)를 분 단위로 변환했다.

# 밀리세컨드 -> 세컨드
# 분단위로 변환 (그룹화를 위해 몫으로 사용)
data['timestamp'] = data['timestamp'] / 1000 // 60

 

데이터의 3번째 행은 해당 게임에서 게임 시작 2분뒤에 내가 총 235의 경험치를 획득했고, 레벨은 1이었다는 것을 의미한다.

 

3. 유저별 그룹화

유저별로 해당 시간에 평균적으로 몇 레벨인지, 평균적으로 얼마의 경험치를 획득하는지 확인하기 위해 그룹화를 진행했다.

grouped = data.groupby(['유저', 'timestamp'])[['level', 'xp']].mean().reset_index()

 

데이터의 3번째 행은 게임 시작 2분뒤에 평균적으로 내가 1.06레벨이고, 총 251의 경험치를 획득 한다는 뜻이다.

 

4. 나와 프로와의 인게임 시간대별 평균 레벨 비교 시각화

이번에도 5대미드로 유명한 페이커, 쵸비, 제가, 비디디, 쇼메이커 선수들의 평균과 나를 비교했다.

 

초반에는 레벨 격차가 크게 벌어지는 것 같아 보이긴 한다. 하지만 확실히 프로 선수들의 평균보다는 낮은 평균 레벨을 기록한다는 것을 알 수 있다. 또한 20분 이후부터 평균 레벨 격차가 벌어지기 시작하는데, 라인전 단계 이후 사이드 운영을 할 때 내가 지속적인 성장을 하지 못하고 있다는 것을 보여준다.

 

또한 항상 만렙인 18레벨을 기록한 시간대가 어느정도인지 확인해 봤다.

 

나는 45분 이후에는 항상 18레벨을 기록했지만, 프로 선수들의 평균치는 40분 이후에 항상 18레벨을 기록하고 있었다. 확실히 프로 선수들이 사이드 운영에서 성장을 잘 챙겨가고 있었다.

 

5. 나와 프로와의 인게임 시간대별 총 경험치 획득량 비교 시각화

다음으로 나와 프로 선수 평균의 시간대별 총 경험치 획득량을 비교하기 위한 시각화를 진행했다.

 

그 어느 순간도 총 경험치 획득량에서 프로의 평균을 넘어본 적이 없었다.

 

6. 증분값 계산 및 시각화

지난 미니언 편 때도 시간이 흐를수록 미니언 수 차이가 누적되기 때문에 뒷부분의 차이가 커져갔었기 때문에 증분값을 계산해서 비교했었다. 이번 총 경험치 획득량도 마찬가지다. 뒤로갈수록 경험치 획득량 차이가 누적되기 때문에, 증분값을 활용해서 내가 1분동안 평균적으로 얼마의 경험치를 획득하는 지에 대한 데이터를 만들었다. 즉, 지금 계산한 증분값은 분당 경험치 획득량이다.

grouped['xp_diff'] = grouped.groupby('유저')['xp'].diff().fillna(0)

 

3번째 행의 xp_diff(증분값 변수)는 내가 인게임 시간 1분 ~ 2분 사이에 평균적으로 250의 경험치를 획득한다는 것을 의미한다.

 

그 다음 계산한 증분값을 활용해 나와 프로 선수들의 분당 경험치 획득량을 비교하는 시각화를 진행했다.

 

이제 시간이 흐를수록 획득했던 경험치 차이가 누적되지 않기 때문에 시간대별로 비교가 가능해졌다. 간단하게 살펴보면 30분전까지는 프로 선수의 성장에 한참 뒤쳐진다. 30분 이후로는 내가 성장을 앞서가는 부분도 있지만 분당 경험치 획득이 300이하로 떨어지는 부분도 있다. 프로 선수들은 아무리 낮아도 500근처를 유지하는데, 나는 성장이 아예 멈추는 구간이 너무 많다. 데스를 기록해서 부활 대기시간이 늘어나 성장을 못하고 있거나, 사이드 운영을 아예 못하고 있는 것 같다.

 

이제 10분 단위로 끊어서 자세하게 살펴보자.

 

먼저 10분 이전 분당 경험치 획득량을 시각화 했다.

 

게임 시작 4분만에 경험치 획득 차이가 벌어진다. 4분은 첫갱 타이밍, 첫 바위게 싸움 타이밍이다. 여기서 데스를 자주 기록하면서 경험치 획득량 차이가 벌어진 것 같다. 정글러의 위치를 특정할 수 있는 첫 와드를 잘 사용해야 하고, 바위게 싸움에서 아무리 우리 정글러가 떼를 쓰더라도 데스를 기록해서는 절대 안된다는 생각으로 게임을 해야한다.

 

다음은 10분 ~ 20분 사이의 경험치 획득량 차이를 시각화했다.

 

처참하다. 이건 뭐 어느 부분을 집어서 이야기 할 수가 없다. 한마디로 이 구간을 해석하려면 최대한 죽지 말자라는 마인드로 게임을 해야겠다.

 

다음은 20분 ~ 30분 사이의 경험치 획득량 차이를 시각화했다.

 

3용 ~ 4용 싸움이 치열해지는 24분 ~ 26분 사이에는 성장이 비슷하지만 그 전후로 경험치 획득량이 떨어지는 모습을 보이고 있다. 생각해보면 사이드 운영할 때 내가 먼저 라인 푸쉬를 하는 경우가 많이 없는 것 같다. 그러다 보니 라인 손해가 발생해서 이렇게 되는 것 같다. 물론 내가 자주하는 메이지 챔피언의 특성 때문일 수 있다. 그러므로 사이드 할 때 시야를 잘 잡고, 우리팀과 적팀의 정글러와 서포터의 위치를 잘 파악하는 습관을 들여야 겠다.

 

마지막으로 30분 이후 경험치 획득량을 시각화 했다.

 

프로 선수와 달리 나의 경험치 획득량이 위 아래로 급격하게 흔들리는 모습을 볼 수 있다. 이 구간에서는 고점을 높이겠다는 생각보다는 저점을 낮추겠다는 생각으로 게임을 해야겠다. 즉, 죽지 않는 것과 더불어 라인 손해를 최대한 안보는 방식으로 플레이를 해야겠다.

 

 

 

프로 선수와 달리 나는 레벨 성장이 멈추는 구간이 많았다.

쿠키

선수들의 평균이 아니라 선수 개개인별로 평균을 비교해봤다.

 

20분 전까지는 프로 선수들이 비슷한 수치를 보이는 데 20분 넘어가는 순간 쵸비의 성장력이 정말 압도적이다. 라인전의 신이라고 불리는 쵸비인데, 후반 성장력도 어마어마 하다. 너무 사기 아닌가..? 진짜 이번 롤드컵 쵸비가 우승할수도..?

 

쵸비의 라인전 영상만 집중해서 보곤 했는데, 후반 위치와 사이드 운영에 대해서도 집중해서 봐야겠다.


마무리

<지금까지 분석한 결론들 정리>

1. 오브젝트 싸움 전 죽어도 된다는 마인드로 더 적극적인 포지션 선점

2. 장신구 와드 쿨타임 돌때마다 사용

3. 5 ~ 15분 타이밍에 더 많은 제어와드 사용

4. 라인전 처음 미니언 6마리 집중해서 다 먹기

5. 19분 타이밍에 라인받고, 텔레포트 활용해서 오브젝트 싸움 참여

6. 24분 타이밍엔 라인 안받고, 오브젝트 쪽 시야싸움 합류

7. 4분 첫 갱타이밍, 첫 바위게타이밍에 첫 와드 통해서 상대 정글 위치 파악하고, 바위게 싸움에서는 죽지않는 마인드

8. 사이드 운영할 때 상대와 우리 정글, 서폿 위치 파악

9. 게임 내내 죽으면 안된다는 생각과, 라인 손해를 최소화 한다는 생각으로 플레이


최종코드

# 라이브러리

!pip install koreanize_matplotlib
import koreanize_matplotlib

import pandas as pd
import numpy as np

import requests

import time
import datetime

from urllib import parse # 한글

import matplotlib.pyplot as plt
import seaborn as sns

from tqdm import tqdm

import warnings
warnings.filterwarnings('ignore')
api_key = "API_KEY_HERE" # 새로 발급받은 api_key
REQUEST_HEADERS = {
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36",
  "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
  "Accept-Charset": "application/x-www-form-urlencoded; charset=UTF-8",
  "Origin": "https://developer.riotgames.com",
  "X-Riot-Token": api_key
}
# 프로선수들과 비교 최종코드

proplayer_list = ['바아른 곰', 'Hide on bush', '허거덩', 'DK ShowMaker', '아구몬', 'dlwldms']
tag_list = ['KR1', 'KR1', '0303', 'KR1', '0509', 'iuiu']

유저 = ['본인', '페이커', '쵸비', '쇼메이커', '비디디', '제카']
event_list = []

for idx in tqdm(range(len(proplayer_list))):
  # puuid 불러오기

  userNickname = proplayer_list[idx] # 유저 닉네임 입력

  tagLine = tag_list[idx] # 유저 태크 입력

  encodedName = parse.quote(userNickname) # API 요청을 위한 유저 닉네임 인코딩

  url = f"https://asia.api.riotgames.com/riot/account/v1/accounts/by-riot-id/{encodedName}/{tagLine}"

  player_id = requests.get(url, headers=REQUEST_HEADERS).json()
  time.sleep(1.3)

  puuid = player_id['puuid'] # 플레이어 고유 아이디

  # 매치ID 불러오기

  match_ids = []

  for i in range(0, 4):
    match_url = f"https://asia.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?start={i * 100}&count=100"
    match_response = requests.get(match_url, headers=REQUEST_HEADERS)
    time.sleep(1.3)
    if match_response.status_code == 200:
      match_ids.extend(match_response.json())


  # 데이터 수집
  for match_id in match_ids:

    # 맵, 라인 확인
    url = f"https://asia.api.riotgames.com/lol/match/v5/matches/{match_id}"
    response = requests.get(url, headers=REQUEST_HEADERS)
    time.sleep(1.3)
    matchinfo = response.json()

    # 맵ID 가 협곡인지 확인
    if matchinfo['info']['mapId'] != 11:
      continue

    # 라인을 미드로 플레이했는지 확인
    participants = matchinfo['info']['participants']
    is_mid = False
    for participant in participants:
      if participant['puuid'] == puuid:
        if participant['individualPosition'] == 'MIDDLE' and participant['teamPosition'] == 'MIDDLE':
          is_mid = True
          break
    if is_mid == False:
          continue


    url = f"https://asia.api.riotgames.com/lol/match/v5/matches/{match_id}/timeline"  # 매치 로그에 대한 데이터 API
    response = requests.get(url, headers=REQUEST_HEADERS)
    time.sleep(1.3)
    timeline = response.json()

    # participantId 찾기
    for user in timeline['info']['participants']:
      if user['puuid'] == puuid:
        participantId = user['participantId']

    # 프레임별 미니언 처치 수 찾기
    for frame in timeline['info']['frames']:
      event = {}
      event['matchId'] = match_id
      event['timestamp'] = frame['timestamp']
      event['xp'] = frame['participantFrames'][str(participantId)]['xp']
      event['level'] = frame['participantFrames'][str(participantId)]['level']
      event['유저'] = 유저[idx]
      event_list.append(event)
# 밀리세컨드 -> 세컨드
# 분단위로 변환 (그룹화를 위해 몫으로 사용)
data['timestamp'] = data['timestamp'] / 1000 // 60

# 그룹화
# 유저별로 분당 평균 미니언 처치 수 계산
grouped = data.groupby(['유저', 'timestamp'])[['level', 'xp']].mean().reset_index()
# 레벨 시각화
df = grouped.loc[grouped['유저'] == '본인']

# 프로 평균 지표 계산
pro_df = grouped.loc[grouped['유저'] != '본인'].groupby('timestamp')['level'].mean().reset_index()
pro_df['유저'] = '프로평균'

df = pd.concat([df, pro_df])

plt.figure(figsize = (8, 4))
for user in df['유저'].unique():
  user_data = df[df['유저'] == user]
  plt.plot(
      user_data['timestamp'],
      user_data['level'],
      label = user
  )

plt.title('나와 프로의 분단위 레벨 변화 비교')
plt.xlabel('게임 시간 (분 단위)')
plt.ylabel('레벨')

plt.grid()
plt.legend(title = '유저')
plt.show()
# 경험치 시각화
df = grouped.loc[grouped['유저'] == '본인']

# 프로 평균 지표 계산
pro_df = grouped.loc[grouped['유저'] != '본인'].groupby('timestamp')['xp'].mean().reset_index()
pro_df['유저'] = '프로평균'

df = pd.concat([df, pro_df])

plt.figure(figsize = (8, 4))
for user in df['유저'].unique():
  user_data = df[df['유저'] == user]
  plt.plot(
      user_data['timestamp'],
      user_data['xp'],
      label = user
  )

plt.title('나와 프로의 분단위 총 경험치 획득량 비교')
plt.xlabel('게임 시간 (분 단위)')
plt.ylabel('총 경험치')

plt.grid()
plt.legend(title = '유저')
plt.show()
# 증분값 계산
grouped['xp_diff'] = grouped.groupby('유저')['xp'].diff().fillna(0)

# 증분값으로 분당 경험치 획득량 시각화
df = grouped.loc[grouped['유저'] == '본인']

# 프로 평균 지표 계산
pro_df = grouped.loc[grouped['유저'] != '본인'].groupby('timestamp')['xp_diff'].mean().reset_index()
pro_df['유저'] = '프로평균'

df = pd.concat([df, pro_df])

plt.figure(figsize = (8, 4))
for user in df['유저'].unique():
  user_data = df[df['유저'] == user]
  plt.plot(
      user_data['timestamp'],
      user_data['xp_diff'],
      label = user
  )

plt.title('나와 프로의 분당 경험치 획득량 비교')
plt.xlabel('게임 시간 (분 단위)')
plt.ylabel('경험치 획득량')

plt.grid()
plt.legend(title = '유저')
plt.show()