문제 1
우리 과제는 최동원의 롯데 시절 1983년부터 1988년의 데이터를 사용하도록 하겠습니다.
최동원의 당시 데이터와 함께 1983년부터 1988년 사이 투수들의 데이터(연봉포함)를 구하고, 2015년부터 2020년까지 프로야구의 투수들의 데이터를 구하시오.
해결 방법
저는 우선 야구선수들의 데이터가 필요하다 판단하여 KBO 사이트에서 데이터를 찾기로 하였습니다.
해당 사이트 확인 결과 TABLE로 구성되어 있는 것을 확인하였습니다. 그래서 Selenium과 BeautifulSoup를 활용하면 될 거라 판단하였습니다.
즉, 첫 번째 작업으로 해당 테이블을 크롤링을 목표로 잡았습니다.
from selenium import webdriver
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
from selenium.webdriver.support.select import Select
import time
import pandas as pd
import numpy as np
url = 'https://www.koreabaseball.com/Record/Player/PitcherBasic/BasicOld.aspx'
# 창 열기
driver = webdriver.Chrome()
driver.get(url)
driver.maximize_window()
# KBO 정규시즌 선택
select_season = Select(driver.find_element(By.ID, 'cphContents_cphContents_cphContents_ddlSeries_ddlSeries'))
select_season.select_by_index(0)
table = []
for year in range(1983, 1989):
time.sleep(1)
# 1986년도 선택
select_year = Select(driver.find_element(By.ID, 'cphContents_cphContents_cphContents_ddlSeason_ddlSeason'))
select_year.select_by_value(str(year))
time.sleep(2)
# 해당 페이지 읽기
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
# 해당 페이지 즉 html의 table태그의 내용들을 모두 가져오고 문자열로 변환
tables = soup.findAll('table')
table.append(str(tables[0]))
driver.quit()
이렇게 하면 문제에서 원하는 1983년부터 1988년의 최동원 선수의 데이터와 다른 투수들 데이터를 모두 크롤링 하였습니다.
그리고 총 6개의 테이블은 현재 tables리스트에 들어있습니다.
두 번째 작업으로는 tables에 있는 모든 html들을 각각 변수를 만든 후 DataFrame으로 저장하겠습니다.
# 동적으로 변수명 지정 후 해당 변수들에게 dataframe 적용
table_year = 1983
for i in range(len(table)):
globals()['stats_table_{}'.format(table_year)] = pd.read_html(table[i])[0]
table_year += 1
저는 변수명을 하나하나 만들지 말고 차라리 반복문을 통해 동적 변수를 만든 후 한 번에 넣는 방식을 사용하였습니다.
그래서 stats_table_1983 ~ stats_table_1988 변수들에 모두 dataframe을 저장할 수 있었습니다.
세 번째 작업으로는 info()를 통해 결측치 확인 및 타입 확인을 하고 수치형 데이터들은 모두 실수형 or 정수형으로 바꿔주겠습니다.
# IP가 문자열 -> IP를 실수형 변환 -> 150 2/3 = 150 + 2/3 -> 이렇게 변환
table_year = 1983
for i in range(len(table)):
data = globals()['stats_table_{}'.format(table_year)]
for idx, value in data.iterrows():
ip_data = data['IP'].iloc[idx].split(" ")
if len(ip_data) == 2:
data['IP'].iloc[idx] = float(ip_data[0]) + round(eval(ip_data[1]), 2)
else :
data['IP'].iloc[idx] = float(ip_data[0]) + 0.0
table_year += 1
# 실수로 계산 하였지만 계속 object타입으로 변환되어 직접 float형으로 변환
table_year = 1983
for i in range(len(table)):
data = globals()['stats_table_{}'.format(table_year)]
data['IP'] = data['IP'].astype('float')
table_year += 1
우선, IP를 실수형 데이터로 변환한 이유 :
해당 문제는 최동원 선수가 현역 선수라면 연봉이 얼마일 것이가에 초점을 두었습니다. 그 말은 현역 선수들의 데이터들의 모든 값들을 비교해야 할 것입니다.
만약 해당 컬럼(IP)이 연봉 수치에 영향을 주는 컬럼이라면 무조건 비교해야 할 수치라 생각되어 실수형 데이터로 변환 하였습니다.
문자열 계산을 위해 eval()함수를 사용하였습니다.
네 번째 작업은 2015년 ~ 2020년 투수들의 데이터를 모두 크롤링 한 후 위에서 했던 작업 그대로 똑같이 해보겠습니다.
# 창 열기
driver = webdriver.Chrome()
driver.get(url)
driver.maximize_window()
select_year = Select(driver.find_element(By.ID, 'cphContents_cphContents_cphContents_ddlSeries_ddlSeries'))
select_year.select_by_index(0)
table = []
salaries = []
for year in range(2015, 2021):
time.sleep(1)
# 2015 ~ 2020년 선택
select_year = Select(driver.find_element(By.ID, 'cphContents_cphContents_cphContents_ddlSeason_ddlSeason'))
select_year.select_by_value(str(year))
time.sleep(1)
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
tables = soup.findAll('table')
table.append(str(tables[0]))
body = driver.find_element(By.CLASS_NAME, 'tData01.tt')
tbody = body.find_element(By.TAG_NAME, 'tbody')
rows = tbody.find_elements(By.TAG_NAME, 'tr')
for i in range(len(rows)):
name = rows[i].find_elements(By.TAG_NAME, 'td')
name[1].click()
# 선수들 연봉 긁어오기
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
name = soup.find('span', {'id':'cphContents_cphContents_cphContents_playerProfile_lblName'})
salary = soup.find('span', {'id': 'cphContents_cphContents_cphContents_playerProfile_lblSalary'})
retire = soup.find('span', {'id':'cphContents_cphContents_cphContents_ucRetireInfo_lblName'})
if salary:
if retire:
salaries.append({retire.text:salary.text})
else :
salaries.append({name.text:salary.text})
else :
if retire:
salaries.append({retire.text:np.nan})
else :
salaries.append({name.text:np.nan})
driver.back()
tbody = driver.find_element(By.TAG_NAME, 'tbody')
rows = tbody.find_elements(By.TAG_NAME, 'tr')
driver.quit()
# 동적으로 변수명 지정 후 해당 변수들에게 dataframe 적용
table_year = 2015
for i in range(len(table)):
globals()['stats_table_{}'.format(table_year)] = pd.read_html(table[i])[0]
table_year += 1
# IP가 문자열 -> IP를 실수형 변환 -> 150 2/3 = 150 + 2/3 -> 이렇게 변환
table_year = 2015
for i in range(len(table)):
data = globals()['stats_table_{}'.format(table_year)]
for idx, value in data.iterrows():
ip_data = data['IP'].iloc[idx].split(" ")
if len(ip_data) == 2:
data['IP'].iloc[idx] = float(ip_data[0]) + round(eval(ip_data[1]), 2)
else :
data['IP'].iloc[idx] = float(ip_data[0]) + 0.0
table_year += 1
table_year = 2015
for i in range(len(table)):
data = globals()['stats_table_{}'.format(table_year)]
data['IP'] = data['IP'].astype('float')
table_year += 1
위의 내용과 같은 코드이지만 내용이 추가 되었습니다. 연봉을 긁어오는 작업이 추가 되었습니다.
다섯 번째 작업은 최신 데이터에서는 WHIP컬럼이 추가된 것으로 확인 되었습니다. 하지만 옛날 선수들 데이터에는 없는 것으로 확인되어 옛날 선수 데이터에도 WHIP을 추가해주겠습니다.
# WHIP구하기 -> data['WHIP'] = (data['BB'] + data['H']) / data['IP']
table_year = 1983
for i in range(len(table)):
data = globals()['stats_table_{}'.format(table_year)]
data['WHIP'] = (data['BB'] + data['H']) / data['IP']
table_year += 1
WHIP을 구한 이유?
WHIP 컬럼을 빼지 않고 구한 이유 또한 위에서 IP를 실수형으로 변경한 이유와 같습니다.
해당 컬럼 또한 연봉 계산에 영향있는 컬럼일 경우를 생각하여 추가해주었습니다.
WHIP 계산 방법 : (BB(볼넷) + H(피안타)) / IP(이닝) 입니다.
여섯 번째 작업은 선수들의 연봉을 뽑으면서 중복된 값들이 있습니다. 해당 중복을 제거해주겠습니다.
# 중복 연봉 제거
remove_duplicated_salaries = [dict(t) for t in {tuple(d.items()) for d in salaries}]
우선 각각의 딕셔너리에 저장된 연봉 값을 꺼내주고 해당 값들을 튜플을 사용하여 타입 변환을 해줍니다.(튜플은 중복 허용이 안되어 알아서 제거됨.)
그리고 다시 딕셔너리로 저장해줍니다.
마지막 작업은 선수들의 연봉을 어떻게 할지 정하는 것입니다.
하지만 해당 작업에서 문제가 몇 개 있었습니다. 우선 옛날 선수들과 은퇴한 선수들의 연봉은 나오지 않는 것이였습니다.
- 우선 현역 선수들 중에서 연봉이 안나오는 경우는 연봉 사이트에서 크롤링하는 방법(정확도⬆️)
- nan값으로 된 곳들을 전부 해당 년도 투수들의 평균 연봉으로 넣을 것인가.(매우 빠른 작업)
- 옛날 선수들의 데이터는 찾기가 매우 어려우며 당시 연봉과 현재 연봉과의 차이는 매우 클 것이라는 추측으로 저는 옛날 선수들의 연봉 데이터는 뽑지 않기로 결정하였습니다.
즉, 1번을 시도해도 연봉이 안나오는 선수는 제가 수기로 찾아서 데이터를 넣을 것입니다. 그리고 옛날 선수들의 연봉은 뽑지 않겠습니다.
이 글은 제로베이스 데이터 분석 취업 스쿨의 강의 자료 일부를 발췌하여 작성되었습니다.
다음 글에서는 위에서 계획 세운 내용들을 적용해보겠습니다.
이상입니다.
'Project > Machine Learning' 카테고리의 다른 글
Project - Instacart 데이터 물품 재구매 예측하기(RFM) (0) | 2024.11.19 |
---|---|
Project - Instacart 데이터 물품 재구매 예측하기 (0) | 2024.11.18 |
[Zero-base] 최동원 선수 연봉 예측하기 - 보충 (1) | 2024.10.29 |
[Zero-base] 최동원 선수 연봉 예측하기 - 3 (1) | 2024.10.21 |
[Zero-base] 최동원 선수 연봉 예측하기 - 2 (0) | 2024.10.21 |