[OPGG] 챔피언 군집화하기

Updated:

챔피언 군집화

이번 강의에선 각 챔피언을 군집화하여 특정 챔피언을 선호하는 사람에게 다른 챔피언을 추천할 수 있게 분석을 하였다.

챔피언 군집화 기준으론 챔피언별 구입한로 아이템을 사용한다.

정확하게 이번 강의는 출력할 형태를 알려주고 직접 코드를 작성하는 방식으로 7,8일차 동안 진행하였다.

# 패키지 로드
import os
import numpy as np
import pandas as pd
# import pymysql
import requests
import math
from matplotlib import pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
# 라이엇 개발자 페이지에서 게임 상수 최신 버전 정보 가져오기
constant_patch = requests.get("https://ddragon.leagueoflegends.com/api/versions.json").json()[0]

# constant_patch == '11.16.1'

# 라이엇 개발자 페이지에서 최신 버전 한국 서버 아이템 정보 가져오기
item_info = requests.get(f"http://ddragon.leagueoflegends.com/cdn/{constant_patch}/data/ko_KR/item.json").json()

# 라이엇 개발자 페이지에서 최신 버전 한국 서버 챔피언 정보 가져오기
champion_info = requests.get(f"http://ddragon.leagueoflegends.com/cdn/{constant_patch}/data/ko_KR/champion.json").json()

# item_info, champion_info는 json형태
  • 항상 최신 버전을 불러오기 위해 버전 정보를 변수로 사용하여 아이템, 챔피언 정보를 불러왔다.
# 챔피언 정보 => DataFrame으로 변환
champion_df = pd.DataFrame(champion_info['data']).T[['key','name']]
champion_df = champion_df.reset_index(drop=True)
# champion_df는 'key'로 id값, 'name'으로 챔피언 한국어 이름을 가지는 156rows짜리 DataFrame

# champion_df의 'key' column string => numeric 변환
champion_df['key'] = pd.to_numeric(champion_df['key'])

1. 아이템 가격 데이터

우선 각 아이템의 가격 데이터를 만들 것이다.

아이템은 하위템이 존재하고 여기서 작업하는 바는 특정 템의 하위템과 조합비용 등을 출력하는 것이다.

자세한 것은 조금 후에 알 수 있다.

1.1 방법1 (보완필요)

우선 이 방법은 처음 가이드라인으로 준 방식이 아닌 완전 새롭게 해보려고 직접 작성한 코드이다.

뒤의 결과로 나타나지만 약간의 보완은 필요하지만 괜찮은 방법인 것 같다.

방법2가 가이드라인이고 자세한 설명이 적혀있다.

# consumed가 null인 경우만
item = pd.DataFrame(item_info['data']).T[['name','gold','tags','from','consumed']]
item2 = item[item["consumed"].isnull()].drop(columns="consumed", axis=1)
item2
name gold tags from
1001 장화 {'base': 300, 'purchasable': True, 'total': 30... [Boots] NaN
1004 요정의 부적 {'base': 250, 'purchasable': True, 'total': 25... [ManaRegen] NaN
1006 원기 회복의 구슬 {'base': 150, 'purchasable': True, 'total': 15... [HealthRegen] NaN
1011 거인의 허리띠 {'base': 500, 'purchasable': True, 'total': 90... [Health] [1028]
1018 민첩성의 망토 {'base': 600, 'purchasable': True, 'total': 60... [CriticalStrike] NaN
... ... ... ... ...
6692 월식 {'base': 850, 'purchasable': True, 'total': 32... [Damage, LifeSteal, SpellVamp, NonbootsMovemen... [3134, 1036, 1053]
6693 자객의 발톱 {'base': 1000, 'purchasable': True, 'total': 3... [Damage, Active, CooldownReduction, NonbootsMo... [3134, 3133]
6694 세릴다의 원한 {'base': 650, 'purchasable': True, 'total': 32... [Damage, CooldownReduction, ArmorPenetration, ... [3133, 3035]
6695 독사의 송곳니 {'base': 625, 'purchasable': True, 'total': 26... [Damage, ArmorPenetration] [3134, 1037]
8001 증오의 사슬 {'base': 800, 'purchasable': True, 'total': 25... [Health, Active, CooldownReduction, AbilityHaste] [3067, 1011]

188 rows × 4 columns

  • 우선 json 파일에 consumed라는 키가 존재하면 제거하였다(소모품).

  • pd.json_normalize()를 사용하지 않았고 여기서 gold, tags, from 컬럼의 값은 문자가 아닌 dictionary, list이다.

  • gold에서 base는 조합비용, total은 전체 구입비용이다.

  • 자세한 것은 본인이 직접 뜯어보고 이해하여야 한다.

# tags에 Consumable이 있으면 제외
item3 = item2[item2["tags"].apply(lambda x: False if "Consumable" in x else True)]
item3
name gold tags from
1001 장화 {'base': 300, 'purchasable': True, 'total': 30... [Boots] NaN
1004 요정의 부적 {'base': 250, 'purchasable': True, 'total': 25... [ManaRegen] NaN
1006 원기 회복의 구슬 {'base': 150, 'purchasable': True, 'total': 15... [HealthRegen] NaN
1011 거인의 허리띠 {'base': 500, 'purchasable': True, 'total': 90... [Health] [1028]
1018 민첩성의 망토 {'base': 600, 'purchasable': True, 'total': 60... [CriticalStrike] NaN
... ... ... ... ...
6692 월식 {'base': 850, 'purchasable': True, 'total': 32... [Damage, LifeSteal, SpellVamp, NonbootsMovemen... [3134, 1036, 1053]
6693 자객의 발톱 {'base': 1000, 'purchasable': True, 'total': 3... [Damage, Active, CooldownReduction, NonbootsMo... [3134, 3133]
6694 세릴다의 원한 {'base': 650, 'purchasable': True, 'total': 32... [Damage, CooldownReduction, ArmorPenetration, ... [3133, 3035]
6695 독사의 송곳니 {'base': 625, 'purchasable': True, 'total': 26... [Damage, ArmorPenetration] [3134, 1037]
8001 증오의 사슬 {'base': 800, 'purchasable': True, 'total': 25... [Health, Active, CooldownReduction, AbilityHaste] [3067, 1011]

184 rows × 4 columns

  • tags는 아이템에 대한 태그 설명으로 Consumable이 존재하면 삭제하여 4건을 더 지웠다.
# item_df 완성
item_df = item3.copy()
item_df["base_gold"] = item3["gold"].apply(lambda x: x['base'])
item_df["total_gold"] = item3["gold"].apply(lambda x: x['total'])

item_df.drop(columns=["gold","tags"], inplace=True)
item_df
name from base_gold total_gold
1001 장화 NaN 300 300
1004 요정의 부적 NaN 250 250
1006 원기 회복의 구슬 NaN 150 150
1011 거인의 허리띠 [1028] 500 900
1018 민첩성의 망토 NaN 600 600
... ... ... ... ...
6692 월식 [3134, 1036, 1053] 850 3200
6693 자객의 발톱 [3134, 3133] 1000 3200
6694 세릴다의 원한 [3133, 3035] 650 3200
6695 독사의 송곳니 [3134, 1037] 625 2600
8001 증오의 사슬 [3067, 1011] 800 2500

184 rows × 4 columns

  • 각 아이템의 기본 정보 item_df를 만들었다.
# item_tree 완성
item_tree = pd.DataFrame(item_df["from"].apply(lambda x: pd.Series(x)).stack()).reset_index(level=1, drop=True)
item_tree = item_tree.reset_index()
item_tree.rename(columns={"index":"id", 0:"id_from"}, inplace=True)
item_tree
id id_from
0 1011 1028
1 1031 1029
2 1043 1042
3 1043 1042
4 1053 1036
... ... ...
295 6694 3035
296 6695 3134
297 6695 1037
298 8001 3067
299 8001 1011

300 rows × 2 columns

  • item_tree는 각 아이템의 하위 템을 id_from으로 가지는 데이터 프레임이다.

  • stack된 형태로 생성하였으며 하위 템이 없는 아이템은 id에 포함되지 않는다.

# index 컬럼으로 바꿔주기
item_df = item_df.reset_index()
item_df.rename(columns={'index':"id"}, inplace=True)
item_df
id name from base_gold total_gold
0 1001 장화 NaN 300 300
1 1004 요정의 부적 NaN 250 250
2 1006 원기 회복의 구슬 NaN 150 150
3 1011 거인의 허리띠 [1028] 500 900
4 1018 민첩성의 망토 NaN 600 600
... ... ... ... ... ...
179 6692 월식 [3134, 1036, 1053] 850 3200
180 6693 자객의 발톱 [3134, 3133] 1000 3200
181 6694 세릴다의 원한 [3133, 3035] 650 3200
182 6695 독사의 송곳니 [3134, 1037] 625 2600
183 8001 증오의 사슬 [3067, 1011] 800 2500

184 rows × 5 columns

  • 아이템 id를 컬럼으로 바꿔주었다.
#item_df[item_df["id"]=="4643"]
# left join으로 하위템에 아이템을 붙혔을 때 하위템이 있는 경우만 남기기
# 즉, 추가한 id_from이 NaN이 아닌 경우
item_tree_full = item_tree
item_tree_new = item_tree

# 하위템에 하위템이 있는 경우를 고려해서 while문으로 작성
while True:
    temp = pd.merge(item_tree_new, item_tree_full, left_on="id_from", right_on="id", how="left")
    temp2 = temp[["id_x","id_from_y"]]

    if temp2["id_from_y"].notnull().sum() == 0:
        break

    item_tree_new = temp2[temp2["id_from_y"].notnull()]
    item_tree_new.rename(columns={"id_x":"id","id_from_y":"id_from"}, inplace=True)

# 기존 item_tree에 row를 추가
item_tree_full = pd.concat([item_tree_full, item_tree_new], axis=0)
item_tree_full
C:\Users\ekzm3\anaconda3\lib\site-packages\pandas\core\frame.py:4296: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  return super().rename(
id id_from
0 1011 1028
1 1031 1029
2 1043 1042
3 1043 1042
4 1053 1036
... ... ...
369 6694 1036
370 6695 1036
371 6695 1036
373 8001 1028
374 8001 1028

507 rows × 2 columns

  • 위 과정은 item_tree에서 하위템의 하위템이 존재하니 이를 merge를 통해 최종 하위템을 붙혀준다.

  • while을 사용한 이유는 하위템의 하위템의 하위템… 등 때문이나 여기선 한번만 작동한다.

  • 현재 라이엇 아이템은 최종 - 하위 - 하위 구조로 이루어져 있기 때문이다.

  • 기존 300개의 item_tree에서 207개의 row가 추가되었다.

# 자기 자신 추가해주기
id_self = pd.DataFrame([item_tree_full["id"].unique(), item_tree_full["id"].unique()]).T
id_self.columns = item_tree_full.columns
item_tree_full = pd.concat([item_tree_full, id_self], axis=0)
item_tree_full = item_tree_full.sort_values(by="id")
item_tree_full
id id_from
0 1011 1028
0 1011 1011
1 1031 1029
1 1031 1031
2 1043 1042
... ... ...
373 8001 1028
374 8001 1028
299 8001 1011
298 8001 3067
131 8001 8001

639 rows × 2 columns

  • 자기 자신을 추가하였다.

  • 예를 들면 유령무희 - 유령무희를 추가해두었다.

  • 이런 형태를 만든 이유는 조합비용 컬럼을 사용하기 위함이다.

# 아이템별 골드 추가해주기
#pd.merge(item_tree_full)
item_tree_full2 = pd.merge(item_tree_full, item_df, left_on="id_from", right_on="id")
item_tree_full2
id_x id_from id_y name from base_gold total_gold
0 1011 1028 1028 루비 수정 NaN 400 400
1 2065 1028 1028 루비 수정 NaN 400 400
2 3001 1028 1028 루비 수정 NaN 400 400
3 3044 1028 1028 루비 수정 NaN 400 400
4 3050 1028 1028 루비 수정 NaN 400 400
... ... ... ... ... ... ... ...
633 6692 6692 6692 월식 [3134, 1036, 1053] 850 3200
634 6693 6693 6693 자객의 발톱 [3134, 3133] 1000 3200
635 6694 6694 6694 세릴다의 원한 [3133, 3035] 650 3200
636 6695 6695 6695 독사의 송곳니 [3134, 1037] 625 2600
637 8001 8001 8001 증오의 사슬 [3067, 1011] 800 2500

638 rows × 7 columns

item_tree_full3 = item_tree_full2.groupby(["id_x", "id_from"]).sum().reset_index()
item_tree_full3 = item_tree_full3[["id_x","id_from","base_gold"]]
item_tree_full3.rename(columns={"id_x":"id"}, inplace=True)
#temp[temp["id"]=="3046"]
item_tree_full3
id id_from base_gold
0 1011 1011 500
1 1011 1028 400
2 1031 1029 300
3 1031 1031 500
4 1043 1042 600
... ... ... ...
537 6695 6695 625
538 8001 1011 500
539 8001 1028 800
540 8001 3067 400
541 8001 8001 800

542 rows × 3 columns

  • 각 아이템, 하위템별로 골드 합계를 구하였다.

  • 예를 들어 유령무희는 하위템으로 단검이 2개 필요하기에 이를 반영한 것이다.

  • 여기서 id별로 합계를 구하면 해당 아이템의 전체 비용을 구할 수 있을 것이다.

1.2 방법2

# pd.json_normalize(item_info['data']['1011'], record_path=["tags"])
# 아이템 정보 => DataFrame으로 변환
item_df = pd.DataFrame(columns=['id','name','gold'])

# item_df는 아이템의 숫자 id, 한국어 이름(name) 및 조합비(직전 하위템 전부 갖고 있을 때 완성템을 조합할 때 드는 비용)를 가지는 DataFrame

item_tree = pd.DataFrame(columns=['id','id_from'])

# item_tree는 아이템의 숫자 id, 직전 하위템 리스트(id_from)를 가지는 DataFrame
# 예시: 유령 무희(2600G) = 롱소드(350G) + 열정의 검(1050G) + 롱소드(350G) + 850G이므로 유령 무희에 대한 rows가 3개 나옴

for item_id in item_info['data']:
    # Consumable한 아이템(체력 물약 등) 제외
    # 'tags'에 'Consumable' 정보가 없는 아이템에 대해서만 수행
    if item_info['data'][item_id]['tags'].count("Consumable") == 0:
        try:
            # 'consumed' 필드가 있는 아이템(비스킷)은 하단 코드가 실행되지 않고 continue로 바로 다음 item_id로 넘어감
            item_info['data'][item_id]['consumed']
            #작성#
            continue
        except:
            pass
        
        # 아이템명 및 조합비
        # id, name, gold 3개 필드를 가지게끔 json parsing
        name = item_info['data'][item_id]['name']
        base_gold = item_info['data'][item_id]['gold']["base"]
        
        item_row = pd.DataFrame([item_id, name, base_gold]).T
        item_row.columns = item_df.columns
        
        item_df = pd.concat([item_df,item_row])

        # 직전 하위템
        try:
            # 'from' 필드에 있는 항목을 각각 하나의 row로 가지는 item_row_tree DataFrame 생성
            item_row_tree = pd.DataFrame()
            item_row_tree["id_from"] = item_info['data'][item_id]['from']
            item_row_tree["id"] = item_id
            item_row_tree = item_row_tree[["id","id_from"]]
            # 작성
            ###
            item_tree = pd.concat([item_tree, item_row_tree])
        # 하위템이 없는 경우 생략
        except Exception:
            pass
  • 이 부분은 가이드라인을 보고 내가 생각한대로 작성한 코드이다.
# 아이템 정보 => DataFrame으로 변환
item_df = pd.DataFrame(columns=['id','name','gold'])

# item_df는 아이템의 숫자 id, 한국어 이름(name) 및 조합비(직전 하위템 전부 갖고 있을 때 완성템을 조합할 때 드는 비용)를 가지는 DataFrame

item_tree = pd.DataFrame(columns=['id','id_from'])

# item_tree는 아이템의 숫자 id, 직전 하위템 리스트(id_from)를 가지는 DataFrame
# 예시: 유령 무희(2600G) = 롱소드(350G) + 열정의 검(1050G) + 롱소드(350G) + 850G이므로 유령 무희에 대한 rows가 3개 나옴

for item_id in item_info['data']:
    # Consumable한 아이템(체력 물약 등) 제외
    # 'tags'에 'Consumable' 정보가 없는 아이템에 대해서만 수행
    if "Consumable" not in pd.json_normalize(item_info['data'][item_id],record_path=['tags']).values:
        try:
            # 'consumed' 필드가 있는 아이템(비스킷)은 하단 코드가 실행되지 않고 continue로 바로 다음 item_id로 넘어감
            pd.json_normalize(item_info['data'][item_id]).consumed
            #작성#
            continue
        except:
            pass
        
        # 아이템명 및 조합비
        # id, name, gold 3개 필드를 가지게끔 json parsing
        item_row = pd.json_normalize(item_info['data'][item_id])[['name','gold.base']]
        item_row["id"] = item_id
        item_row.rename(columns={"gold.base":"gold"},inplace=True)
        
        
        item_df = pd.concat([item_df,item_row])

        # 직전 하위템
        try:
            # 'from' 필드에 있는 항목을 각각 하나의 row로 가지는 item_row_tree DataFrame 생성
            item_row_tree = pd.json_normalize(item_info['data'][item_id], record_path=["from"])
            item_row_tree.rename(columns={0:"id_from"},inplace=True)
            item_row_tree["id"] = item_id
            
            # 작성
            ###
            item_tree = pd.concat([item_tree, item_row_tree])
        # 하위템이 없는 경우 생략
        except Exception:
            pass
<ipython-input-16-1135ef07f429>:14: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
  if "Consumable" not in pd.json_normalize(item_info['data'][item_id],record_path=['tags']).values:
  • 이건 강사님께서 시간을 주신 후 설명해주신 방법대로 작성한 코드이다.

  • pd.json_normalize()를 사용한 방식이 다르다.

  • 강사님이 적어주신 주석을 통해 출력할 데이터 프레임 형태를 만들었다.

  • 앞서 방법1은 이를 알고 새로 작성한 것이기에 설명이 부족했을 수 있겠다.

item_df
id name gold
0 1001 장화 300
0 1004 요정의 부적 250
0 1006 원기 회복의 구슬 150
0 1011 거인의 허리띠 500
0 1018 민첩성의 망토 600
... ... ... ...
0 6692 월식 850
0 6693 자객의 발톱 1000
0 6694 세릴다의 원한 650
0 6695 독사의 송곳니 625
0 8001 증오의 사슬 800

184 rows × 3 columns

item_tree
id id_from
0 1011 1028
0 1031 1029
0 1043 1042
1 1043 1042
0 1053 1036
... ... ...
1 6694 3035
0 6695 3134
1 6695 1037
0 8001 3067
1 8001 1011

300 rows × 2 columns

item_tree_full = item_tree
item_tree_new = item_tree

# 가장 하위 아이템까지 full 아이템트리 및 각 재료 아이템에 소모되는 골드를 item_tree_full DataFrame에 저장
# 예시로 이 셀 최종 시점에 유령 무희에 대해서는 다음과 같은 rows가 나옴
# id            | id_from          | gold
# 3046(유령 무희) | 3046(유령 무희)    | 850
# 3046(유령 무희) | 3086(열정의 검)    | 150
# 3046(유령 무희) | 1036(롱소드)       | 700
# 3046(유령 무희) | 1018(민첩성의 망토) | 600
# 3046(유령 무희) | 1042(단검)        | 300

while True:
    # item_tree_new에 item_tree를 merge해서 방금 추가한 하위템보다 한 depth 아래의 하위템 리스트 추출
    item_tree_new = item_tree_new.merge(item_tree, left_on="id_from",right_on="id")[["id_x","id_from_y"]].rename(columns={"id_x":"id","id_from_y":"id_from"})
    
    # 모두 재료템만 남았으면 종료
    if len(item_tree_new) == 0:
        break
    item_tree_full = pd.concat([item_tree_full, item_tree_new])

# 자기자신을 리스트에 추가
item_tree_new['id']=item_df['id']
item_tree_new['id_from']=item_df['id']
item_tree_full = pd.concat([item_tree_full, item_tree_new])

# 각 아이템의 조합비를 merge
item_tree_full = item_tree_full.merge(item_df, left_on="id_from",right_on="id")[["id_x","id_from","gold"]].rename(columns={"id_x":"id"})

# 중복되는 row가 발생할 시 gold 수치를 합쳐서 중복되는 row 제거
item_tree_full = item_tree_full.groupby(["id","id_from"], as_index=False).sum("gold")

# 유령 무희 | 롱소드 | 350
# 유령 무희 | 롱소드 | 350
# -->
# 유령 무희 | 롱소드 | 700
item_tree_full
id id_from gold
0 1001 1001 300
1 1004 1004 250
2 1006 1006 150
3 1011 1011 500
4 1011 1028 400
... ... ... ...
589 6695 6695 625
590 8001 1011 500
591 8001 1028 800
592 8001 3067 400
593 8001 8001 800

594 rows × 3 columns

  • 여기까지 최종적으로 아이템 가격 데이터를 만들었다.

  • 그런데 방법1과는 다르게 row가 594개이다.

  • 이는 item_tree의 경우 하위템이 없으면 id가 없는데 강사님은 이런 경우도 추가한 것이다.

  • 나의 경우 row가 더 작은데 나는 하위템이 없는 경우는 추가하지 않았기 때문이다.

  • 뒤에 작업시 강사님 형태로 만드는 것이 편하기에 방법1은 약간의 보완이 필요한 것이다.

# string => numeric 형변환
item_df['id'] = pd.to_numeric(item_df['id'])
item_tree_full['id'] = pd.to_numeric(item_tree_full['id'])
item_tree_full['id_from'] = pd.to_numeric(item_tree_full['id_from'])
# 메모리 확보를 위해 사용하지 않을 변수 삭제
del constant_patch
del champion_info
del item_id
del item_info
del item_row
del item_row_tree
del item_tree
del item_tree_new
# # OPGG Database에 connection 생성
# con = pymysql.connect(
#     user = os.environ['LOL_KR_ID'],
#     passwd = os.environ['LOL_KR_PW'],
#     host = os.environ['LOL_KR_HOST'],
#     db = 'lol',
#     charset = 'utf8'
# )
# cursor = con.cursor(pymysql.cursors.DictCursor)
# # 패치 날짜 데이터 불러오기
# cursor.execute('''
# SELECT version, date
# FROM lolVersionHistory
# ''')
# patchDate = cursor.fetchall()

# patchDate = pd.DataFrame(patchDate)

# # version => season, patch로 가공, 세부 버전(핫픽스, 밸런싱 X 패치 등) 제거
# patchDate['season'] = pd.to_numeric(patchDate['version'].str.split('.').str[0])
# patchDate['patch'] = pd.to_numeric(patchDate['version'].str.split('.').str[1])
# patchDate = patchDate.groupby(['season','patch'],as_index=False).min('date').drop('version', axis = 1).sort_values(['season','patch'])
# # 현재 패치 칼바람 챔피언&아이템 데이터 불러오기
# cursor.execute('''
# SELECT STRAIGHT_JOIN championId, item0, item1, item2, item3, item4, item5
# FROM opGame o FORCE INDEX (ix_createDate),
# p_opGameStats p FORCE INDEX (`PRIMARY`)
# WHERE o.gameId = p.gameId
# AND o.createDate >= '{lastpatch}'
# AND p.createDate >= '{lastpatch}'
# AND subType = 450
# '''.format(lastpatch=pd.to_datetime(patchDate['date'].tail(1).values[0])))
# gamestats = cursor.fetchall()

# gamestats = pd.DataFrame(gamestats)

# # # csv로 저장
# # gamestats.to_csv("gamestats.csv", mode='w')

2. 플레이 정보

OPGG 데이터베이스에 있는 데이터를 이용해 챔피언별로 어떤 아이템을 샀는지 확인할 것이다.

# csv 파일에서 가져오기
gamestats = pd.read_csv('Day07_01_gamestats.csv').drop('Unnamed: 0',axis=1)
gamestats.head()
championId item0 item1 item2 item3 item4 item5
0 43 6653 3089 0 0 2422 0
1 517 2420 0 6656 1052 3191 3111
2 122 6630 3111 3044 1037 1028 0
3 80 2031 3111 3134 6692 3123 0
4 7 1026 2031 0 3020 4628 3802
gamestats.shape
(12700160, 7)
  • 약 1,270만 데이터로 각 챔피언이 게임별로 어떤 아이템을 샀는지 확인 가능하다.
# 아이템 컬럼 하나로 모으기
# 'pandas.melt'라는 함수를 적용
# gamestats에 item0 ~ item5로 나와있는 컬럼을 item 컬럼으로 합쳐서 한 소환사당 1*6 형태로 된 DataFrame을 6*1로 변환
# gamestats.set_index("championId").stack().reset_index().drop("level_1", axis=1)
itemstats = pd.melt(gamestats, id_vars=['championId'], value_name='item').drop("variable", axis=1)

# itemstats에는 챔피언id가 'championId'로, 구매한 아이템이 'item' column으로 들어감

# 빈 값(0) 지우기
itemstats = itemstats[itemstats.item != 0]
itemstats.head()
championId item
0 43 6653
1 517 2420
2 122 6630
3 80 2031
4 7 1026
del gamestats
# 약간 신비한 신발 => 신발 치환
itemstats.item[itemstats.item == 2422] = 1001

# 초시계 시리즈 => 초시계 치환
itemstats.item[itemstats.item.isin([2419, 2421, 2423, 2424])] = 2420

# 무라마나 => 마나무네 치환
# 대천사의 포옹 => 대천사의 지팡이 치환
itemstats.item[itemstats.item == 3042] = 3004
itemstats.item[itemstats.item == 3040] = 3003

# 오른의 걸작 치환
itemstats.item[itemstats.item == 7000] = 6693
itemstats.item[itemstats.item == 7001] = 6692
itemstats.item[itemstats.item == 7002] = 6691
itemstats.item[itemstats.item == 7003] = 6664
itemstats.item[itemstats.item == 7004] = 3068
itemstats.item[itemstats.item == 7005] = 6662
itemstats.item[itemstats.item == 7006] = 6671
itemstats.item[itemstats.item == 7007] = 6672
itemstats.item[itemstats.item == 7008] = 6673
itemstats.item[itemstats.item == 7009] = 4633
itemstats.item[itemstats.item == 7010] = 4636
itemstats.item[itemstats.item == 7011] = 3152
itemstats.item[itemstats.item == 7012] = 6653
itemstats.item[itemstats.item == 7013] = 6655
itemstats.item[itemstats.item == 7014] = 6656
itemstats.item[itemstats.item == 7015] = 6630
itemstats.item[itemstats.item == 7016] = 6631
itemstats.item[itemstats.item == 7017] = 6632
itemstats.item[itemstats.item == 7018] = 3078
itemstats.item[itemstats.item == 7019] = 3190
itemstats.item[itemstats.item == 7020] = 2065
itemstats.item[itemstats.item == 7021] = 6617
itemstats.item[itemstats.item == 7022] = 4005
  • 사실상 똑같은 아이템의 id를 바꿔주었다.
itemstats.groupby(["championId","item"], as_index=False).size()
championId item size
0 1 1001 1857
1 1 1004 79
2 1 1006 137
3 1 1011 1732
4 1 1018 66
... ... ... ...
23671 887 6692 3
23672 887 6693 2
23673 887 6694 2
23674 887 6695 10
23675 887 8001 81

23676 rows × 3 columns

# 챔피언별로 각 아이템에 투자한 골드 총량 계산

# championId, item를 기준으로 grouping하여 row 수를 'size' column을 만들어 저장
# itemstats = itemstats.groupby(["championId","item"]).size().reset_index()
# itemstats.rename(columns={0:"size"}, inplace=True)
itemstats = itemstats.groupby(["championId","item"], as_index=False).size()

# item_tree_full과 merge하여 각 아이템에 맵핑되는 조합비(gold) column 추가
itemstats = pd.merge(itemstats, item_tree_full, left_on="item", right_on="id", how="left").sort_values(by="championId")

# 해당 아이템의 조합비가 적혀 있는 'gold' column에 해당 아이템을 사는데 소모한 총 골드(gold * size) 덮어쓰기
itemstats['gold'] = itemstats["size"] * itemstats["gold"]

# 이후 계산 알아보기 쉽도록 scaling
# Consine similarity를 쓰기 때문에 value scaling은 영향 없음
itemstats['gold'] = itemstats['gold'] / 1e5

# size column 제거
itemstats = itemstats.drop('size',axis=1)

# item_tree_full과 merge할 때 생긴 중복 row 합치기
# itemstats = itemstats.groupby(['championId','id_from'], as_index=False).apply(lambda x: pd.Series({'gold':sum(x.gold)}))
itemstats = itemstats.groupby(['championId','id_from']).sum()["gold"].reset_index()
itemstats.head()
championId id_from gold
0 1 1001.0 218.1090
1 1 1004.0 3.8825
2 1 1006.0 1.3665
3 1 1011.0 54.4700
4 1 1018.0 6.7380
# 챔피언 골드 비례 상수 계산
# 각 벡터의 norm을 계산하는 것
championstats = itemstats.groupby('championId',as_index=False).apply(lambda x: pd.Series({'length':
                                                                                         math.sqrt(sum(x.gold**2))}))
championstats.head()
championId length
0 1 2441.439769
1 2 596.649753
2 3 1493.691225
3 4 3376.113961
4 5 1000.377577
# 각 챔피언 간 유사도 계산을 위한 내적(inner product) 계산 수행

# itemstats 자기자신을 아이템 기준으로 merge하여 champion X와 champion Y가 해당 아이템을 사는데 소모한 골드를 한 row에 저장
itemstats = pd.merge(itemstats,itemstats, on="id_from")[["championId_x","championId_y","gold_x","gold_y"]]
# itemstats == [championId_x | championId_y | gold_x | gold_y]

# championId_x > championId_y인 row만 남기기
# itemstats = itemstats[itemstats.apply(lambda x: True if x["championId_x"] > x["championId_y"] else False, axis=1)]
itemstats = itemstats[itemstats["championId_x"] > itemstats["championId_y"]]

# 각 row에 대해 gold_x와 gold_y를 곱해서 gold_prod 컬럼에 저장
itemstats['gold_prod'] = itemstats["gold_x"] * itemstats["gold_y"]

# 다 사용한 gold columns 제거
itemstats = itemstats.drop(['gold_x','gold_y'],axis=1)

# championId_x, championId_y를 기준으로 grouping하여 gold_prod 합 계산
itemstats = itemstats.groupby(["championId_x","championId_y"]).sum("gold_prod").reset_index()
# itemstats == [championId_x | championId_y | gold_prod]
# 156 x 155 / 2 
itemstats
championId_x championId_y gold_prod
0 2 1 1.556730e+05
1 3 1 1.550968e+06
2 3 2 5.182713e+05
3 4 1 7.096273e+06
4 4 2 3.250152e+05
... ... ... ...
12085 887 526 2.063231e+05
12086 887 555 3.964452e+05
12087 887 777 2.452904e+05
12088 887 875 1.136604e+06
12089 887 876 2.232326e+06

12090 rows × 3 columns

# 챔피언 유사도 및 거리 계산

# itemstats에 championstats를 merge해서 length_x, length_y 컬럼 추가
itemstats = pd.merge(itemstats, championstats, left_on="championId_x",right_on="championId", how="left").rename(columns={"length":"length_x"})
itemstats.drop("championId", axis=1, inplace=True)
itemstats = pd.merge(itemstats, championstats, left_on="championId_y",right_on="championId", how="left").rename(columns={"length":"length_y"})
itemstats.drop("championId", axis=1, inplace=True)
# itemstats == [championId_x | championId_y | gold_prod | length_x | length_y]

# similarity = gold_prod / (length_x * length_y) column 추가
itemstats['similarity'] = itemstats["gold_prod"] / (itemstats["length_x"] * itemstats["length_y"])

# distance = arccos(similarity)/(pi/2) column 추가
itemstats['distance'] = np.arccos(itemstats['similarity']) / (np.pi/2)

# 필요한 column(챔피언간 거리)만 추출
itemstats = itemstats[['championId_x','championId_y','distance']]
itemstats
championId_x championId_y distance
0 2 1 0.931836
1 3 1 0.720337
2 3 2 0.604904
3 4 1 0.339766
4 4 2 0.896831
... ... ... ...
12085 887 526 0.788218
12086 887 555 0.974986
12087 887 777 0.956487
12088 887 875 0.822705
12089 887 876 0.417589

12090 rows × 3 columns

# distance array 형태로 변환
# index에 주의

# itemstats를 championId_x, championId_y 내림차순으로 정렬하여 'distance' column만 남겨두기
distance_array = itemstats.sort_values(['championId_x','championId_y'], ascending=[False,False])['distance']
distance_array
12089    0.417589
12088    0.822705
12087    0.956487
12086    0.974986
12085    0.788218
           ...   
4        0.896831
3        0.339766
2        0.604904
1        0.720337
0        0.931836
Name: distance, Length: 12090, dtype: float64
# 'complete' 메소드로 h-clustering 수행
Z = linkage(distance_array, 'complete')
# 시각화
fig = plt.figure(figsize=(25, 10))
dn = dendrogram(Z)

# 적당한 개수의 클러스터로 구분
cluster = pd.DataFrame(fcluster(Z,0.6,criterion='distance'))
cluster.columns=['cluster']
# 결과 DataFrame 구성

# key column 기준 내림차순으로 된 champion_df.name를 cluster 오른쪽에 concat
result = pd.concat([cluster, champion_df.sort_values(by="key", ascending=False)["name"].reset_index(drop=True)], axis=1)
# result == [cluster | name]
# 출력
pd.set_option('display.max_rows', None)
result
cluster name
0 6 그웬
1 4 릴리아
2 7 세트
3 2 요네
4 1 파이크
5 8
6 2 아펠리오스
7 4 니코
8 4 사일러스
9 8 오른
10 2 자야
11 8 라칸
12 3 바드
13 2 칼리스타
14 5 아이번
15 7 렉사이
16 7 일라오이
17 8 쓰레쉬
18 2 사미라
19 4 유미
20 4 아지르
21 5 나미
22 7 아트록스
23 7 바이
24 1 키아나
25 4 에코
26 7 클레드
27 1 제드
28 2 루시안
29 2 세나
30 7 비에고
31 8 탐 켄치
32 2 징크스
33 2 킨드레드
34 2
35 8 브라움
36 2 아크샨
37 7 카밀
38 4 탈리야
39 4 벨코즈
40 2 야스오
41 8 자크
42 7 나르
43 4 세라핀
44 3 카이사
45 4 자이라
46 4 조이
47 1 케인
48 4 아우렐리온 솔
49 4 신드라
50 2
51 4 다이애나
52 4 리산드라
53 1 제이스
54 7 다리우스
55 1 카직스
56 7 헤카림
57 2 드레이븐
58 5 룰루
59 4 직스
60 7 피오라
61 8 세주아니
62 4 빅토르
63 8 노틸러스
64 1 바루스
65 1 렝가
66 8 볼리베어
67 4 피즈
68 2 그레이브즈
69 4 아리
70 4 쉬바나
71 4 제라스
72 4 럭스
73 8
74 2 코그모
75 7 리븐
76 1 탈론
77 4 말자하
78 8 레오나
79 7 가렌
80 4 케넨
81 6 아칼리
82 7 요릭
83 6 모데카이저
84 1 이즈리얼
85 1 판테온
86 4 그라가스
87 8 뽀삐
88 8 우디르
89 4 니달리
90 6 나서스
91 4 하이머딩거
92 8 스카너
93 4 카시오페아
94 4 럼블
95 2 베인
96 7 리 신
97 4 브랜드
98 7 오공
99 4 오리아나
100 4 엘리스
101 7 자르반 4세
102 7 레넥톤
103 6 마오카이
104 1 녹턴
105 6 카타리나
106 4 말파이트
107 6 블리츠크랭크
108 2 케이틀린
109 4 스웨인
110 7 트런들
111 4 베이가
112 8 타릭
113 4 카르마
114 1 코르키
115 1 갱플랭크
116 4 잔나
117 7 이렐리아
118 4 카사딘
119 5 소나
120 8 문도 박사
121 4 샤코
122 4 애니비아
123 8 람머스
124 8 아무무
125 8 초가스
126 4 카서스
127 2 트위치
128 4 이블린
129 6 신지드
130 4 질리언
131 4 모르가나
132 7 잭스
133 2 트린다미어
134 1 애쉬
135 3 미스 포츈
136 4 누누와 윌럼프
137 7 워윅
138 2 트리스타나
139 4 티모
140 8 소라카
141 1 시비르
142 8 사이온
143 4 라이즈
144 8 알리스타
145 1 마스터 이
146 2 케일
147 4 피들스틱
148 4 블라디미르
149 4 르블랑
150 7 우르곳
151 7 신 짜오
152 3 트위스티드 페이트
153 8 갈리오
154 7 올라프
155 4 애니

Leave a comment