[프로젝트] 배틀그라운드 무기추천 프로젝트 (Result)

Colacan·2022년 5월 25일
4

[프로젝트]

목록 보기
8/9
post-thumbnail

Topic : 배틀그라운드 API를 통해서 맵과 지역별로 무기를 추천해주는 프로젝트


PPT와 발표스크립트, 코드파일을 포함한 추가적인 내용은 깃허브 링크를 참조하시길 바랍니다.
깃허브 링크 : https://github.com/colacan100/PUBG_Regional_Weapons_Recommendation_Project

파이프라인

전체적인 과정에 대한 파이프라인 이미지. 세부적인 내용들은 각 파트를 확인바랍니다.


데이터 가져오기

데이터 수집, 저장과정에 대한 이미지입니다. 데이터 가져오기, 데이터 저장 파트가 포함됩니다.

Selenium

셀레니움을 통해서 전적통계사이트인 op.gg 의 서버별 경쟁전 상위 500명의 닉네임을 동적크롤링

import selenium
from selenium import webdriver
from selenium.webdriver import ActionChains

from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By

from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.ui import WebDriverWait

URL = 'https://pubg.op.gg/?hl=ko_KR'

driver = webdriver.Chrome(executable_path='chromedriver')
driver.get(url=URL)

driver.implicitly_wait(time_to_wait=5)

def append_name(x):
    search_one = driver.find_elements_by_css_selector('a.leader-board-top3__nickname')
    for i in search_one:
        name = i.text
        x.append(name)
    search_many = driver.find_elements_by_css_selector('a.leader-board__nickname')
    for i in search_many:
        name = i.text
        x.append(name)

search_more = driver.find_element_by_xpath('//*[@id="steamtpp4"]/div[2]/a')
search_more.click()
steam_compete = []
append_name(steam_compete)

# 링크를 변경하여 다른 게임모드의 데이터도 가져올 수 있다.
click_compute_solo = driver.find_element_by_xpath('/html/body/div[1]/section/div[3]/div/div/div[2]/div/div/a[3]')
click_compute_solo.click()
steam_compete_solo = []
append_name(steam_compete_solo)

PUBG developer API

배틀그라운드 개발자 API를 통해서 유저닉네임별 ID 추출

def player_id(nickname,match_list):
  try:
    import requests
    url = f"https://api.pubg.com/shards/steam/players?filter[playerNames]={nickname}"
    header = {
      "Authorization": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJmZThiOWIyMC05ZDRhLTAxM2EtYzQ3Yi0wZTVmZGQ2ODc3NTYiLCJpc3MiOiJnYW1lbG9ja2VyIiwiaWF0IjoxNjQ5ODQ5NDA3LCJwdWIiOiJibHVlaG9sZSIsInRpdGxlIjoicHViZyIsImFwcCI6InB1YmdfYXBpX3Byb2plIn0.jwbNcForczhmcOWRdeBqSY5NlrE9yTErGmTycPHUzNw",
      "Accept": "application/vnd.api+json"
    }
    r = requests.get(url, headers=header)
    player_dataitgirls_json = r.json()
    data = player_dataitgirls_json['data'][0]['relationships']['matches']['data']
    for i in data:
      match_list.append(i['id'])
  except:
    pass

유저닉네임별로 최근 매치정보 ID 추출

def player_match(id):
    import requests
    url = f'https://api.pubg.com/shards/steam/matches/{id}'
    header = {
      "Authorization": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJmZThiOWIyMC05ZDRhLTAxM2EtYzQ3Yi0wZTVmZGQ2ODc3NTYiLCJpc3MiOiJnYW1lbG9ja2VyIiwiaWF0IjoxNjQ5ODQ5NDA3LCJwdWIiOiJibHVlaG9sZSIsInRpdGxlIjoicHViZyIsImFwcCI6InB1YmdfYXBpX3Byb2plIn0.jwbNcForczhmcOWRdeBqSY5NlrE9yTErGmTycPHUzNw",
      "Accept": "application/vnd.api+json"
    }
    r = requests.get(url, headers=header)
    player_dataitgirls_json = r.json()
    return player_dataitgirls_json

Chicken dinner API

매치정보 ID를 통해서 세부매치정보 조회
세부매치정보 중 킬로그 데이터 추출

import time
from chicken_dinner.pubgapi import PUBG

api_key = "사용자 API Key"

def kill_ver2(platform,match_id,for_list):
    try:
        pubg = PUBG(api_key=api_key, shard=f"{platform}")
        match = pubg.match(f"{match_id}")
        telemetry = match.get_telemetry()
        kill_events = telemetry.filter_by("log_player_kill_v2")
        rosters = match.rosters
        rank_list = {}
        for roster in rosters:
            rank_list[roster.player_ids[0]] = roster.stats['rank']
        for kill in kill_events:
            try:
                info = kill['killer_damage_info']
                kill_dict = {
                'map' : telemetry.map_name(),
                'rank': rank_list.get(kill['killer']["account_id"]),
                'zone': kill['killer']['zone'][0],
                'additional_info': info['additional_info'],
                'damage_causer_name': info['damage_causer_name'],
                'damage_reason': info['damage_reason'],
                'distance': info['distance']
                }
                for_list.append(kill_dict)
            except:
                pass
    except:
        time.sleep(1)
        pass
import time
from tqdm import tqdm

def kill_mongodb_ver2(match_list,platform,data_list):
    for match_id in tqdm(match_list):
        kill_ver2(f'{platform}',f'{match_id}',data_list)
steam_compete_match_list = []
steam_data = []

for i in tqdm(steam_compete_solo):
    player_id(i,steam_compete_match_list)

kill_mongodb_ver2(steam_compete_match_list,'steam',steam_data)

데이터 저장

NoSQL - MongoDB

NoSQL 데이터베이스 중 하나인 MongoDB에 추출한 데이터 저장

  • Directory명 : PUBG_MONGODB
  • Database명: pubgdata
  • Collection명 : steam_data
import pymongo
# 몽고db 연결
conn = pymongo.MongoClient("사용자의 mongodb url")
pubgdata = conn.pubgdata
# 컬렉션 사용
steam_result_list = pubgdata.steam_data
steam_result_list.insert_many(steam_data)

만들어진 db 형식

{
    "_id": {
        "$oid": "625febde77348e530d8cdf2b"
    },
    "map": "Tiger_Main",
    "rank": {
        "$numberInt": "36"
    },
    "zone": "palace",
    "additional_info": [],
    "damage_causer_name": "WeapMini14_C",
    "damage_reason": "HeadShot",
    "distance": {
        "$numberDouble": "416.1879577636719"
    }
}

SQL - SQLite

Database명: pubg_result.db

# 스키마
_id VARCHAR NOT NULL PRIMARY KEY,
map VARCHAR,
rank INTEGER,
zone VARCHAR,
damage_causer_name VARCHAR,
damage_reason VARCHAR,
distance REAL,
additional_info VARCHAR

mongodb의 데이터를 sqlite로 sql형식으로 변경

import os
import copy
import sqlite3
from pymongo import MongoClient


DATABASE_NAME = 'pubgdata'
COLLECTION_NAME = 'steam_data'
MONGO_URI = "mongodb+srv://whaleuser:tjddn-100@pubgdata.skatj.mongodb.net/pubgdata?retryWrites=true&w=majority"

DB_FILENAME = 'pubg_result.db'
DB_FILEPATH = os.path.join(os.getcwd(), DB_FILENAME)

client = MongoClient(MONGO_URI)
pubg_db = client[DATABASE_NAME]
pubg_col = pubg_db[COLLECTION_NAME]

conn = sqlite3.connect(DB_FILEPATH)
cur = conn.cursor()

cur.execute('DROP TABLE IF EXISTS PUBG')
cur.execute('''
CREATE TABLE IF NOT EXISTS PUBG (
    _id VARCHAR NOT NULL PRIMARY KEY,
    map VARCHAR,
    rank INTEGER,
    zone VARCHAR,
    damage_causer_name VARCHAR,
    damage_reason VARCHAR,
    distance REAL,
    additional_info VARCHAR
)
''')

pubg_list = pubg_col.find({},{'_id':1, 'map':1, 'rank': 1, 'zone': 1, 'damage_causer_name': 1, 'damage_reason': 1,'distance': 1,'additional_info':1})
for pub in pubg_list :
    cur.execute(f'''
    INSERT OR IGNORE INTO PUBG 
    VALUES(
        "{pub['_id']}",
        "{pub['map']}",
        "{pub['rank']}",
        "{pub['zone']}",
        "{pub['damage_causer_name']}",
        "{pub['damage_reason']}",
        "{pub['distance']}",
        "{pub['additional_info']}"
    )
    ''')

conn.commit()

분석용 대시보드 개발

앞의 DB전환을 통해서 추출된 데이터로 시각화를 진행하는 과정에 대한 이미지입니다. 도커와 Metabase를 이용하였습니다.

Metabase

대시보드 중 하나인 Metabase를 이용하여 MongoDB에 저장된 데이터 시각화
(단순한 SQL 쿼리문이므로 코드는 생략합니다)

PUBG 맵 관련 대시보드

  • 배틀그라운드 경쟁전 맵 종류
  • 많이 나오는 경쟁전 맵 TOP 5
  • 미라마의 주요 교전장소
  • 에란겔의 주요 교전장소
  • 태이고의 교전장소

PUBG 무기 관련 대시보드

  • 배틀그라운드 무기 종류
  • 킬을 많이한 총기 TOP5
  • 에란겔의 선호무기
  • 미라마의 선호무기
  • 태이고의 선호무기


머신러닝 모델 적용

데이터를 통해 모델링을 진행한 후 서비스를 구현하고 AWS EC2로 배포하는 과정까지의 이미지입니다.

SVD(Singular value Decomposition)

특이값 분해 모델 (SVD) 을 통해서 추천시스템 개발
각 맵,지역과 무기를 사용하여서 Rank 타겟을 예측한다.
가장 낮은 Rank, 즉 가장 높은 순위의 무기를 결과로 내보낸다.
학습한 model은 pickle을 통해서 부호화

import sqlite3
import pandas as pd
from surprise import SVD, accuracy
from surprise import Reader, Dataset
from surprise import accuracy
from surprise.model_selection import train_test_split
connection = sqlite3.connect('pubg_result.db')
# 에란겔 모델
data = pd.read_sql("SELECT * FROM PUBG;",connection)
data_erangel = data[data['map'] == 'Erangel (Remastered)']
data_erangel = data_erangel[data_erangel['rank'] != 'None']
data_to_erangel = data_erangel[['zone','damage_causer_name','rank']]
erangel_rating = data_erangel.pivot_table('rank', index = '_id', columns='damage_causer_name').fillna(0)
no_sup_item_ids = [] # 보급제외 무기
sup_item_ids = [] # 보급포함 무기
for i in erangel_rating.columns:
    no_sup_item_ids.append(i)
no_sup_item_ids.remove('WeapGroza_C')
no_sup_item_ids.remove('WeapMk14_C')
no_sup_item_ids.remove('WeapAUG_C')
no_sup_item_ids.remove('WeapM249_C')
no_sup_item_ids.remove('WeapAWM_C')
for i in erangel_rating.columns:
    sup_item_ids.append(i)
reader = Reader(rating_scale= (1, 95))
data = Dataset.load_from_df(df=data_to_erangel, reader=reader)
train, test = train_test_split(data, test_size=.25)
model_erangel = SVD(n_factors=100, n_epochs=20, random_state=123)
model_erangel.fit(train) # model 생성
def for_recommend(ml_zone,ml_weapons,model):
    zone = ml_zone # 선택지역
    actual_rating = 0
    a = 95
    for ml_weapon in ml_weapons :
        result = model.predict(zone, ml_weapon, actual_rating)
        if result.est < a:
            weapon_result = result
            a = result.est
    return weapon_result
recommend = for_recommend('school',no_sup_item_ids,model_erangel)
predict_erangel = model_erangel.test(test)
accuracy.mae(predict_erangel)
accuracy.rmse(predict_erangel)
# 미라마 모델
data = pd.read_sql("SELECT * FROM PUBG;",connection)
data_miramar = data[data['map'] == 'Miramar']
data_miramar = data_miramar[data_miramar['rank'] != 'None']
data_to_miramar = data_miramar[['zone','damage_causer_name','rank']]
miramar_rating = data_miramar.pivot_table('rank', index = '_id', columns='damage_causer_name').fillna(0)
reader = Reader(rating_scale= (1, 95))
data = Dataset.load_from_df(df=data_to_miramar, reader=reader)
train, test = train_test_split(data, test_size=.25)
model_miramar = SVD(n_factors=100, n_epochs=20, random_state=123)
model_miramar.fit(train) # model 생성
predict_miramar = model_miramar.test(test)
accuracy.mae(predict_miramar)
accuracy.rmse(predict_miramar)
# 태이고 모델
data = pd.read_sql("SELECT * FROM PUBG;",connection)
data_teigo = data[data['map'] == 'Tiger_Main']
data_teigo = data_teigo[data_teigo['rank'] != 'None']
data_to_teigo = data_teigo[['zone','damage_causer_name','rank']]
teigo_rating = data_teigo.pivot_table('rank', index = '_id', columns='damage_causer_name').fillna(0)
reader = Reader(rating_scale= (1, 95))
data = Dataset.load_from_df(df=data_to_teigo, reader=reader)
train, test = train_test_split(data, test_size=.25)
model_teigo = SVD(n_factors=100, n_epochs=20, random_state=123)
model_teigo.fit(train) # model 생성
predict_teigo = model_teigo.test(test)
accuracy.mae(predict_teigo)
accuracy.rmse(predict_teigo)
import pickle

# 부호화
with open('model_erangel.pkl','wb') as pickle_file:
    pickle.dump(model_erangel, pickle_file)

with open('model_miramar.pkl','wb') as pickle_file:
    pickle.dump(model_miramar, pickle_file)

with open('model_teigo.pkl','wb') as pickle_file:
    pickle.dump(model_teigo, pickle_file)

웹페이지 구현

메인이미지 클릭시 홈화면으로 돌아옴
맵 선택시 지역리스트 자동 업데이트
맵과 지역을 선택한 후 Click버튼 클릭시 추천무기, 서브이미지 출력
서브이미지 클릭시 상세정보창으로 넘어감
(분량상 페이지 중 하나만 첨부했습니다.)

<!DOCTYPE html>
<html>

<head>
    <!-- CSS only -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
    <!-- JavaScript Bundle with Popper -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW"
        crossorigin="anonymous"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>

<body class="p-3 mb-2 bg-secondary text-white">
    <div class="text-center" style="float: none; margin: 10px auto;">
        <a id="weapon" href="http://15.164.89.222:5000/"><img src={{ url_for('static', filename='pubg.jpg' ) }}></a>
    </div>
    <div class="container row" style="float: none; margin: 10px auto;">
        <div class="col-md-3" style="float: none; margin: 0px auto;">
            <blockquote class="blockquote text-center">
                <p class="font-weight-bold p-3 mb-2 bg-dark text-white rounded"><strong>Weapon for your Chicken</strong>
                </p>
            </blockquote>
            <select class="form-select" id="select1">
                <option value="Default">맵 선택</option>
                <option value="erangel">에란겔</option>
                <option value="miramar">미라마</option>
                <option value="teigo">태이고</option>
            </select>
            <select class="form-select" style="margin:10px auto;" id="select2">
                <option>지역 선택</option>
            </select>
            <div class="container row" style="float: none; margin: 10px auto;">
                <button type="button" onclick="cssChange()" class="btn btn-dark" id="button">Click</button>
            </div>
        </div>
        <p class="text-center font-weight-bold text-white rounded"><strong>{{name_weapon}}</strong></p>
        <div class="text-center">
            <a id="weapon" href="https://pubg.op.gg/weapons/{{title_weapon}}/statistics" style="visibility:hidden"><img src={{ url_for('static',
                    filename='to_go.jpg' ) }}></a>
        </div>
    </div>
</body>
<script>
    $("#select1").change(function () {
        switch (this.value) {
            case 'erangel':
                $("#select2").children().remove();

                $("#select2").append("<option value='field1-bar1'>지역 선택</option>");
                $("#select2").append("<option value='zharki'>자르키</option>");
                $("#select2").append("<option value='georgopol'>게오르고폴</option>");
                $("#select2").append("<option value='hospital'>병원</option>");
                $("#select2").append("<option value='primorsk'>프리모스크</option>");
                $("#select2").append("<option value='quarry'>채석장</option>");
                $("#select2").append("<option value='gatka'>갓카</option>");
                $("#select2").append("<option value='ferrypier'>부두</option>");
                $("#select2").append("<option value='severny'>세베르니</option>");
                $("#select2").append("<option value='shootingrange'>사격장</option>");
                $("#select2").append("<option value='rozhok'>로족</option>");
                $("#select2").append("<option value='pochinki'>포친키</option>");
                $("#select2").append("<option value='ruins'>유적</option>");
                $("#select2").append("<option value='school'>학교</option>");
                $("#select2").append("<option value='stalber'>스탈베르</option>");
                $("#select2").append("<option value='yasnayapolyana'>야스나야 폴야나</option>");
                $("#select2").append("<option value='shelter'>대피소</option>");
                $("#select2").append("<option value='mylta'>밀타</option>");
                $("#select2").append("<option value='farm'>농장</option>");
                $("#select2").append("<option value='kameshki'>카메시키</option>");
                $("#select2").append("<option value='mansion'>저택</option>");
                $("#select2").append("<option value='lipovka'>리포브카</option>");
                $("#select2").append("<option value='prison'>감옥</option>");
                $("#select2").append("<option value='myltapower'>밀타 발전소</option>");
                $("#select2").append("<option value='sosnovkamilitarybase'>소스노브카 군사기지</option>");
                $("#select2").append("<option value='novorepnoye'>노보레프노예</option>");
                break;
            case 'miramar':
                $("#select2").children().remove();

                $("#select2").append("<option value='field2-bar1'>지역 선택</option>");
                $("#select2").append("<option value='alcantara'>알칸타라</option>");
                $("#select2").append("<option value='ruins'>유적</option>");
                $("#select2").append("<option value='trailerpark'>트레일러 주차장</option>");
                $("#select2").append("<option value='elpozo'>엘 포소</option>");
                $("#select2").append("<option value='ladrillera'>라드리예라</option>");
                $("#select2").append("<option value='valledelmar'>바예 델 마르</option>");
                $("#select2").append("<option value='lacobreria'>라 코브레리아</option>");
                $("#select2").append("<option value='craterfields'>크레이터 들판</option>");
                $("#select2").append("<option value='montenuevo'>몬테 누에보</option>");
                $("#select2").append("<option value='chumacera'>추마세라</option>");
                $("#select2").append("<option value='sanmartin'>산 마르틴</option>");
                $("#select2").append("<option value='powergrid'>전력망</option>");
                $("#select2").append("<option value='pecado'>페카도</option>");
                $("#select2").append("<option value='watertreatment'>정수장</option>");
                $("#select2").append("<option value='haciendadelpatron'>아시엔다 델 파트론</option>");
                $("#select2").append("<option value='graveyard'>공동묘지</option>");
                $("#select2").append("<option value='losleones'>로스 레오네스</option>");
                $("#select2").append("<option value='torreahumada'>토레 아우마다</option>");
                $("#select2").append("<option value='cruzdelvalle'>크루스 델 바예</option>");
                $("#select2").append("<option value='minasgenerales'>미나스 헤네랄레스</option>");
                $("#select2").append("<option value='junkyard'>폐차장</option>");
                $("#select2").append("<option value='labendita'>라 벤디타</option>");
                $("#select2").append("<option value='campomilitar'>캄포 밀리타르</option>");
                $("#select2").append("<option value='tierrabronca'>티에라 브롱카</option>");
                $("#select2").append("<option value='elazahar'>엘 아사아르</option>");
                $("#select2").append("<option value='lmpala'>임팔라</option>");
                $("#select2").append("<option value='puertoparaiso'>푸에르토 파라이소</option>");
                $("#select2").append("<option value='prison'>감옥</option>");
                $("#select2").append("<option value='minasdelsur'>미나스 델 수르</option>");
                $("#select2").append("<option value='loshigos'>로스 이고스</option>");
                break;

            case 'teigo':
                $("#select2").children().remove();

                $("#select2").append("<option value='field3-bar1'>지역 선택</option>");
                $("#select2").append("<option value='kangneung'>강능</option>");
                $("#select2").append("<option value='hosan'>호산</option>");
                $("#select2").append("<option value='hosanprison'>호산교도소</option>");
                $("#select2").append("<option value='haemoosa'>해무사</option>");
                $("#select2").append("<option value='hapo'>하포</option>");
                $("#select2").append("<option value='songam'>송암</option>");
                $("#select2").append("<option value='ohyang'>오향</option>");
                $("#select2").append("<option value='buksansa'>북산사</option>");
                $("#select2").append("<option value='godok'>고덕</option>");
                $("#select2").append("<option value='wolsong'>월송</option>");
                $("#select2").append("<option value='yongcheon'>용천</option>");
                $("#select2").append("<option value='palace'>고궁</option>");
                $("#select2").append("<option value='terminal'>터미널</option>");
                $("#select2").append("<option value='shipyard'>조선소</option>");
                $("#select2").append("<option value='school'>학교</option>");
                $("#select2").append("<option value='studio'>스튜디오</option>");
                $("#select2").append("<option value='fishingcamp'>낚시터</option>");
                $("#select2").append("<option value='airport'>공항</option>");
                $("#select2").append("<option value='armybase'>군부대</option>");
                break;
            default:
                $("#select2").append("<option value='field3-bar3'></option>");
        }
    });
</script>
<script>
    $("#button").on("click", function () {
        let url = "http://15.164.89.222:5000/" + $("#select1").val() + "/" + $("#select2").val();
        window.location.href = url;
    })
</script>
</html>

웹 페이지 배포

AWS ec2로 배포한 서비스의 전체적인 사용가이드 이미지입니다.

AWS ec2

클라우드 플랫폼 중 하나인 AWS ec2를 이용하여 배포
로컬의 경우 : http://127.0.0.1:5000/
AWS ec2를 이용한 경우 : http://15.164.89.222:5000/
다른 ip를 이용하여 확인했을 때 정상동작


후기

이번 프로젝트를 통해서 데이터 엔지니어링의 전반적인 흐름을 알게된 것 같다. 그만큼 처음 배운 것도 많았고 새로운 방법도 가장 많이 사용했다. 무수한 오류와 함께 성장한 느낌이다. 짧은 기간으로 인해서 모델에 대한 성능을 잘 고려하지 못한 것이 아쉬운 부분. 이는 추가적으로 개선한 후 업로드하도록 하겠다. 또한 백엔드 부분에서 배우고 싶은 것들이 눈에 보인다. 우선은 클라우드 쪽에 관한 공부를 하고싶다. 이또한 추후 공부하여 업로드할 예정이다.

profile
For DE, DA / There is no royal road to learning

0개의 댓글