| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
- 피파온라인 API
- 데벨챌
- BERTopic
- 트위치
- SBERT
- 문맥을 반영한 토픽모델링
- 데이터넥스트레벨챌린지
- geocoding
- 원신
- KeyBert
- 붕괴 스타레일
- 클래스 분류
- Optimizer
- 다항분포
- 옵티마이저
- 자연어 모델
- 토픽 모델링
- 개체명 인식
- 데이터리안
- LDA
- Roberta
- 조축회
- CTM
- NLP
- 코사인 유사도
- 블루아카이브 토픽모델링
- 포아송분포
- Tableu
- 블루 아카이브
- 구글 스토어 리뷰
- Today
- Total
분석하고싶은코코
최동원 선수 연봉 예측 프로젝트 본문
최동원 선수가 2015 - 2020년에 활동했더라면 연봉을 얼마를 받았을까?
데이터 수집부터 예측하는 과정이 길어 과정의 중간중간 어느정도 생략하고 포스팅하고 자세한 내용은 깃허브에 공유된 ipynb 파일을 확인하시면 됩니다! GitHub : (추가 예정)
1983 - 2020년 KBO 투수 데이터 수집
야구에 대해서 관심이 많은게 아니었기 때문에 어떤 지표들이 투수를 평가하는데 좋은 지표인가에 대해서 알 수 없었기 때문에 선수에 대한 평가 지표를 제공해주는 곳에서 모든 데이터를 우선 불러오고 사용할 지표들을 걸러내기로 정했다. 선수에 대해서 평가하기 위해서는 개인의 데이터도 중요하지만 소속팀의 지표도 중요하다고 생각되어서 Kagle에 정리되어 공유된 팀별 소속 투수들의 평균 데이터가 있어 가져와 사용하기로 하였다.
데이터 출저
- http://www.statiz.co.kr (투수 개인 기록 데이터)
- https://www.kaggle.com/datasets/mattop/korean-baseball-pitching-data-1982-2021 (팀 투수들의 평균 지표)
statiz 데이터 가져오기 - 투수 개인 기록 데이터
크롤링
import pandas as pd
import numpy as np
import requests
from bs4 import BeautifulSoup
import re
import time
from tqdm import tqdm
import selenium
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
# 83-88년도 모든 투수 데이터 가져오기
url = 'http://www.statiz.co.kr/stat.php?opt=0&sopt=0&re=1&ys=1983&ye=1988&se=0&te=&tm=&ty=0&qu=auto&po=0&as=&ae=&hi=&un=&pl=&da=1&o1=WAR&o2=OutCount&de=1&lr=0&tr=&cv=&ml=1&sn=480&si=&cn='
response = requests.get(url)
if response.status_code == 200:
html = response.text
soup = BeautifulSoup(html, 'html.parser')
records = []
birth_list = []
skip_row = 1
for tr in soup.find_all("tr")[1:]:
if tr.find_all("th"):
skip_row+=1
continue
tmp = []
cnt=0
flag_birth = True
for text in tr.find_all("td"):
if cnt == 3:
cnt+=1
continue
add_text = '0' if (text.text.replace(' ', '') == '') else text.text
try:
tmp.append(float(add_text))
except:
tmp.append(add_text)
cnt+=1
#선수 생년월일 데이터
try:
if flag_birth:
birth_list.append(text.find("a")["href"][-10:])
flag_birth = False
except:
pass
if '질문/건의' in str(tmp[0]):
break
records.append(tmp)
skip_row+=1
데이터 프레임 만들기
rank = []; name = []; team =[]; gp = []; cg = []; sho = []; gs = []; win = []; loss = [];
save = []; hold = []; inning = []; runs = []; earned_run = []; batter_faced = [];
hits = []; base2 = []; base3 = []; home_run = []; walk = []; intentional_walk = [];
hb = []; so = []; balk=[]; wild =[]; ERA = []; FIP = []; WHIP = []; ERAplus=[]; FIPplus=[]; WAR = []; WPA = []
status_list = [rank, name, team, gp, cg, sho, gs, win, loss, save, hold, inning, runs, earned_run, batter_faced, hits, base2, base3, home_run, walk, intentional_walk, hb, so, balk, wild, ERA, FIP, WHIP, ERAplus, FIPplus, WAR, WPA]
for record in records:
for idx, status in enumerate(status_list):
if idx == len(records)-1:
break
status.append(record[idx])
team_dict = {
'M' : 'MBC Blue Dragons',
'O' : 'OB Bears',
'롯' : 'Lotte Giants',
'빙' : 'Binggre Eagles',
'삼' : 'Samsung Lions',
'청' : 'Chungbo Pintos',
'태' : 'Pacific Dolphins',
'해' : 'Haitai Tigers',
'해빙' : 'Haitai Tigers Binggre Eagles'
}
year = []
team_name = []
r = re.compile("[0-9]+")
for t in team:
year.append('19'+r.findall(t)[0])
team_name.append(team_dict[r.split(t)[1]])
birth_list = birth_list[:-1]
df_8388 = pd.DataFrame({
'rank' : rank,
'name' : name,
'birth' : birth_list,
'team' : team_name,
'year' : year,
'gp' : gp,
'cg' : cg,
'sho' : sho,
'gs' : gs,
'win' : win,
'loss' : loss,
'save' : save,
'hold' : hold,
'inning' : inning,
'runs' : runs,
'earned_run' : earned_run,
'batter_faced' : batter_faced,
'hits' : hits,
'base2' : base2,
'base3' : base3,
'home_run' : home_run,
'walk' : walk,
'intentional_walk' : intentional_walk,
'hb' : hb,
'so' : so,
'balk' : balk,
'wild' : wild,
'ERA' : ERA,
'FIP' : FIP,
'WHIP' : WHIP,
'ERAplus' : ERAplus,
'FIPplus' : FIPplus,
'WAR' : WAR,
'WPA' : WPA
})
df_8388.head()
구원 WAR 지표 가져오기
구원 WAR지표는 단순히 url만 던져줘서 가져올 수 없었기 때문에 셀레니움을 사용하여 가져왔다.
# 구원 투수 정보 가져오기 - 셀레니움
url_Relief = 'http://www.statiz.co.kr/stat.php?opt=0&sopt=0&re=1&ys=1983&ye=1988&se=0&te=&tm=&ty=0&qu=auto&po=0&as=&ae=&hi=&un=&pl=&da=10&o1=WAR_R&de=1&o2=WAR&lr=0&tr=&cv=&ml=1&sn=500&si=&cn='
driver = webdriver.Chrome(executable_path='../../driver/chromedriver')
driver.get(url=url_Relief)
relief_text = (driver.find_elements(By.ID, 'fixcol')[0].text)
driver.close()
relief_text = relief_text.replace('\n', ' ')
relief_text = relief_text.replace('순 이름 팀 정렬 구원WAR ', '')
relief_list = []
tmp = []
cnt = 0
for r_text in relief_text.split():
tmp.append(r_text)
cnt += 1
try:
if cnt == 4:
float(r_text) #구원WAR 데이터가 없으면 에러 발생
relief_list.append(tmp)
tmp = []
cnt = 0
except:
break
relief_year = []
r = re.compile("[0-9]+")
for t in relief_list:
relief_year.append('19'+r.findall(t[2])[0])
idx_list = []
for i in range(len(relief_list)):
idx_list.append(int(df_8388[(df_8388['name'] == relief_list[i][1]) & (df_8388['year'] == relief_year[i])].index.values[0]))
df_8388['relief_WAR'] = np.nan
for idx in idx_list:
try:
df_8388['relief_WAR'].iloc[idx] =float(relief_list[idx][3])
except:
pass
# 83-88 투수 데이터 저장
df_8388.to_csv('1983-1988년 투수 데이터.csv')
15-20년 데이터는 위에서 url 부분에서 년도만 수정해주어 똑같이 진행하면 되기 떄문에 생략하고 83-88년도 데이터와 다른 부분이 연봉에 대한 데이터를 가져와야 하기 때문에 그 부분만 포스팅합니다.
15-20년 선수 연봉 데이터 가져오기
pitcher_to_salary = []
year = []
salary = []
for idx in tqdm(range(len(df_1520['name']))):
url = 'http://www.statiz.co.kr/player.php?opt=11&name=' + df_1520['name'][idx] + '&birth=' + df_1520['birth'][idx]
response = requests.get(url)
if response.status_code == 200:
html = response.text
soup_salary = BeautifulSoup(html, 'html.parser')
else:
print('!!! Error Request !!!')
if [df_1520['name'][idx], df_1520['birth'][idx]] in pitcher_to_salary:
continue
pitcher_to_salary.append([df_1520['name'][idx], df_1520['birth'][idx]])
tmp_year = []
tmp_salary = []
try:
for s in soup_salary.find('table', class_ = 'table table-striped table-condensed').find_all("tr")[1:]:
tmp_year.append(int(s.find_all("td")[0].text)) #년도
try:
tmp_salary.append(int(s.find_all("td")[1].text.replace(',',''))) #연봉
except:
tmp_salary.append(0)
except:
print("Error Pitcher : ", df_1520['name'][idx], '/', birth_list[idx])
tmp_year.append(0)
tmp_salary.append(0)
year.append(tmp_year)
salary.append(tmp_salary)
time.sleep(0.3)
add_to_salary_idx = []
salary_to_df = []
for idx1, i in enumerate(year):
flag1 = (df_1520['name'] == pitcher_to_salary[idx1][0]) & (df_1520['birth'] == pitcher_to_salary[idx1][1])
for idx2, y in enumerate(i):
if y == 0:
print(pitcher_to_salary[idx1][1])
# add_to_salary_idx.append(birth_list.index(pitcher_to_salary[idx1][1]))
continue
flag2 = df_1520['year'] == y
try:
add_to_salary_idx.append(df_1520[(flag1) & (flag2)].index.values[0])
salary_to_df.append(salary[idx1][idx2])
except:
pass
df_1520 = df_1520.iloc[add_to_salary_idx] # 연봉 데이터 매칭을 위해서 데이터 순서를 맞춰줌
df_1520['salary'] = salary_to_df # 연봉 데이터 매칭
df_1520 = df_1520.sort_index() # 매칭후 다시 원래 인덱스대로 정렬
# 연봉 데이터 추가된 데이터 저장
df_1520.to_csv('2015-2020년 투수 데이터.csv')
15 - 20년 투수들의 연봉 데이터만 가져왔다. 그 이유는 데이터를 크롤링 과정에서 83-88년도 투수 데이터 수와 연봉 정보가 일절 없는 선수의 개수가 일치함으로 연봉에 대한 데이터가 없음을 확인하여 83-88년 투수들의 연봉 데이터는 단 한 개도 없었기 때문에 포스팅 하지 않았다.
Kagle에서 정리된 데이터 가져오기 - 팀 투수들의 평균 지표
path = './data/kbopitchingdata.csv' # 다운로드 받은 파일 경로
kbo_df = pd.read_csv(path)
# 팀 이름 통일 작업
team_df = kbo_df[['year','win_loss_percentage','team']].groupby(['team','year']).count().reset_index()
boom_team = {y : [] for y in range(1982,2022)}
build_team = {y : [] for y in range(1982,2022)}
for y in range(1982,2022):
year_teams_list = team_df[team_df['year']==y]['team'].tolist()
# print("{:4} 년 kbo 소속 팀 리스트 : {} " .format(y, year_teams_list))
try:
if set(year_teams_list) != set(before_teams_list):
# print("In {} year Any team(s) build or boom!!" .format(y))
# print("*"*50)
# print('Build Team(s) : ', set(year_teams_list) - set(before_teams_list))
# print('Boom Team(s) : ', set(before_teams_list) - set(year_teams_list))
# print("*"*50,"\n")
if len(set(year_teams_list) - set(before_teams_list)) != 0:
build_team[y] = list(set(year_teams_list) - set(before_teams_list))
if len(set(before_teams_list) - set(year_teams_list)) !=0:
boom_team[y-1] = list(set(before_teams_list) - set(year_teams_list))
except Exception as e:
print("Maybe Now 1st KBO SEASON...")
before_teams_list = year_teams_list.copy()
# 팀 해체 창설 수가 다른 경우
print('\n---- 팀 해체와 창설 수가 다른 시즌 ----')
for idx, bt in boom_team.items():
if idx ==2021:break
if len(bt) != len(build_team[idx+1]):
print("{} boom team(s) : {}" .format(idx, bt))
print("{} build team(s) : {}\n" .format(idx+1, build_team[idx+1]))
# 팀명이 변경 된 경우 찾기
print('\n---- 팀 해체 후 다음 시즌 창설된 팀 ----')
for idx, bt in boom_team.items():
if idx ==2021:break
if len(bt) != 0 :
print("{} boom team(s) : {}" .format(idx, bt))
print("{} build team(s) : {}\n" .format(idx+1, build_team[idx+1]))
# 현재 사용중인 팀명으로 전부 변경
cnt = 1
for idx, bt in boom_team.items():
if idx ==2021:break
if len(bt) != 0 :
kbo_df.replace(bt[0], build_team[idx+1][0], inplace=True)
print("{:2}. ".format(cnt) ,bt[0], " ->", build_team[idx+1][0])
cnt+=1
# 팀 이름 정리된 데이터 저장
kbo_df.to_csv('KBO_TEAM_DATA.csv')
연봉 예측을 위한 데이터 다듬기
데이터 EDA는 포스팅이 길어져 생략.
데이터 전처리
모든 데이터 팀명 통합
old_team_name = {
'Sammi Superstars' : 'Chungbo Pintos',
'Chungbo Pintos' : 'Pacific Dolphins',
'MBC Blue Dragons' : 'LG Twins',
'Binggre Eagles' : 'Hanwha Eagles',
'Pacific Dolphins' : 'Hyundai Unicorns',
'OB Bears' : 'Doosan Bears',
'Ssangbangwool Raiders' : 'SK Wyverns',
'Haitai Tigers' : 'Kia Tigers',
'Hyundai Unicorns' :'Woori Heroes',
'Woori Heroes' : 'Nexen Heroes',
'Nexen Heroes' : 'Kiwoom Heroes',
'SK Wyverns' : 'SSG Landers'
}
now_team_name = {
'LG 트윈스' : 'LG Twins',
'kt wiz' : 'KT Wiz',
'두산 베어스' : 'Doosan Bears',
'삼성 라이온즈' : 'Samsung Lions',
'NC 다이노스' : 'NC Dinos',
'키움 히어로즈' : 'Kiwoom Heroes',
'SSG 랜더스' : 'SSG Landers',
'한화 이글스' : 'Hanwha Eagles',
'KIA 타이거즈' : 'Kia Tigers',
'롯데 자이언츠' : 'Lotte Giants'
}
# 넥센 -> 키움 / sk -> SSG으로 수정, '키'로 키움 히어로즈로 처리되지 않은 데이터가 존재하여 추가로 수정작업 진행
for idx, t_name in enumerate(pitcher_1520['team']):
tmp_name = t_name
if '넥센 히어로즈' in t_name:
tmp_name = tmp_name.replace('넥센 히어로즈','키움 히어로즈')
if t_name == '키' :
tmp_name = tmp_name.replace('키','키움 히어로즈')
if 'SK 와이번스' in t_name:
tmp_name = tmp_name.replace('SK 와이번스','SSG 랜더스')
pitcher_1520['team'].iloc[idx] = tmp_name
# 영문 이름으로 변경
for idx, t_name in enumerate(pitcher_1520['team']):
tmp_name = t_name
for k,v in now_team_name.items():
if k in t_name:
tmp_name = tmp_name.replace(k,v)
pitcher_1520['team'].iloc[idx] = tmp_name
# 과거 팀명을 현재 사용중인 팀명으로 변경
for idx, t_name in enumerate(pitcher_8388['team']):
tmp_name = t_name
for k,v in old_team_name.items():
if k in tmp_name:
tmp_name = tmp_name.replace(k,v)
pitcher_8388['team'].iloc[idx] = tmp_name
투수 평가를 위한 지표 추가
- 9이닝 당 탈삼진, 볼넷 대한 지표 추가
- 나이에 대한 컬럼은 없기 때문에 년도와 생년월일로 칼럼 추가
# 9이닝당 탈삼진, 볼넷 지표 추가
pitcher_1520['K/9'] = pitcher_1520['so'] * 9 / pitcher_1520['inning']
pitcher_1520['BB/9'] = pitcher_1520['walk'] * 9 / pitcher_1520['inning']
pitcher_8388['K/9'] = pitcher_8388['so'] * 9 / pitcher_8388['inning']
pitcher_8388['BB/9'] = pitcher_8388['walk'] * 9 / pitcher_8388['inning']
# 나이 컬럼 추가
age = []
for idx, rows in pitcher_1520.iterrows():
age.append(rows['year'] - int(rows['birth'][:4]))
pitcher_1520['age'] = age
age = []
for idx, rows in pitcher_8388.iterrows():
age.append(rows['year'] - int(rows['birth'][:4]))
pitcher_8388['age'] = age
데이터 분석에 사용할 데이터 선정
타겟 데이터 - salary(연봉)
사용할 데이터 - gp(게임 수), sho(완봉승), gs(선발), win(승리), inning(이닝 수), ERA(자책 점), WHIP(이닝 당 볼넷과 안타 내준 수), FIP(수비 실책을 제외한 투수 평가 지표), WAR(승리 기여 점수), K/9(9이닝 당 탈삼진 수), BB/9(9이닝 당 볼넷 수)
relief_WAR( 구원 투수의 승리 기여 점수) - 최동원 선수는 구원 투수로 활동도 했기 때문에 해당 지표를 추가하는데 있어서 고민해볼 필요가 있다. 선발로 활동한 투수들의 경우 해당 지표가 0이기 때문에 예측에 방해 요소가 될 것이다. 따라서 선발과 구원 모두 활동했던 선수들만 추려서 예측할때는 해당 지표를 추가할 것.
타겟 데이터와의 상관관계


데이터 예측은 총 3번 진행하였는데 자세한 내용은 GIthub에 ipynb파일에 있고 최종적으로 진행한 과정만 포스팅합니다.
예측을 위한 데이터 정재
- 15 - 20 시즌 중 연봉을 협상한 선수의 데이터만을 사용한다.
- 협상한 선수의 데이터만 사용하더라도 연봉이 바뀐 후 데이터는 협상 이후에 대한 지표들을 갖고 있기 때문에 이 부분을 수정해줘야한다.
- 수정은 협상 전 기록들에 가중치(6:4)를 적용한다.
- 데이터에는 부상에 대한 데이터가 없기 때문에 이 부분을 추가해야줘야하는데 직접 찾아 추가하기에는 시간이 너무 오래걸리기 때문에 고액의 연봉자가 이닝수 혹은 경기수가 적다면 부상이거나 혹은 어떠한 이슈로 인해 지표가 안좋기 때문에 해당 고액 연봉자들에 대해서만 적용.
사용자 정의 함수
# 가중치 계산 함수
def gravi(df_list):
result = []
for df in df_list:
df_len = len(df)
ratio = [1]
for _ in range(1,df_len):
tmp = ratio.pop(-1)
ratio.append(round(tmp*0.6,2))
ratio.append(round(tmp*0.4,2))
result.append(ratio)
return result
# 데이터 프레임에서 각각 선수에 대한 데이터 분리
def div_pit(df):
names = df['name'].unique()
tmp =[]
for name in names:
tmp_df = df[df['name'] == name]
births = tmp_df['birth'].unique()
if len(births) == 1 :
tmp.append(tmp_df)
else:
for birth in births:
tmp_df2 = tmp_df[tmp_df['birth'] == birth]
tmp.append(tmp_df2)
return tmp
# 같은 연봉끼리 묶어주기
def div_salary(df):
salarys = set(df['salary'].values)
tmp = []
for s in salarys:
tmp.append(df[df['salary']==s])
return tmp
연봉 협상자 데이터 만들기
pivote2_1520 = pitcher_1520.pivot_table(index=['name','salary','birth','year'])
# 협상에 대한 데이터프레임 제작을 위해 정렬
pivote2_1520 = pivote2_1520.sort_values(['name','birth','year']).reset_index()
# 연봉 협상한 데이터 추출
check_list = []
names = set(pivote2_1520['name'])
changesalray = []
# 이름, 생년월일을 통해서 중복되지 않은 연봉 값이 2개 이상이라면 해당 데이터들의 인덱스 저장
for name in names:
birth_list = pivote2_1520[pivote2_1520['name'] == name]['birth'].unique()
for birth_day in birth_list:
if (name, birth_day) not in check_list:
check_list.append((name, birth_day))
flag = (pivote2_1520['name']==name) & (pivote2_1520['birth'] == birth_day)
if len(set(pivote2_1520[flag]['salary'].values)) >=2:
changesalray.append(pivote2_1520[flag].index.tolist())
# 추출한 데이터 연봉 매칭시켜 데이터프레임 만들기
to_X = []
for p in changesalray:
modify_data = pivote2_1520.iloc[p[:-1]] # 지난 시즌 성적 데이터. 2020년 데이터에 따른 연봉 데이터(2021)가 없기 때문에 뽑지 않음.
change_sa = pivote2_1520['salary'].iloc[p[1:]].to_list() # 연봉 데이터. 2015년의 연봉은 2014년 데이터가 없기 때문에 뽑지 않음.
modify_data['salary']= change_sa # 연봉과 데이터 매칭
to_X.append(modify_data)
to_X = pd.concat(to_X)
to_X.reset_index(drop=True, inplace=True)
# 15-20년 모든 시즌의 연봉의 평균보다 높은 연봉을 받는데 경기수가 15경기 미만인 선수 리스트
tmp = to_X[to_X['salary']>=np.average(to_X['salary'])]
check_data = tmp[tmp['gp']<14]
check_name = check_data['name'].tolist()
to_X = to_X.drop(index=check_data.index.to_list())
to_X = to_X.reset_index(drop=True)
tmp = div_pit(to_X) # 선수 총 수
tmp2 = [] # 선수의 연봉 협상 데이터
for t in tmp:
tmp2.append(div_salary(t))
tmp3 = [] # 선수의 연봉 협상시 가중치 리스트
for t2 in tmp2:
tmp3.append(gravi(t2))
cols = ['name','age','gp','gs', 'win', 'sho', 'save',
'inning', 'hits', 'ERA', 'WHIP', 'WAR', 'FIP', 'relief_WAR', 'K/9', 'BB/9']
p_list = []
for idx1, t2 in enumerate(tmp2): # 연봉 전체 데이터
for idx2, t in enumerate(t2): # 선수 개인 연봉 데이터
t = t.reset_index(drop=True)
p_list2 = []
for col in cols: #컬럼
sum = 0
flag = True # 연속형, 범주형 구분
for idx3 in range(0,len(t)): #가중치
try:
if col == 'age':
sum += t[col][idx3]
else:
sum += t[col][idx3] * tmp3[idx1][idx2][-(idx3+1)]
except: # 컬럼이 숫자가 아니면 에러 발생
flag = False
break
if flag:
if col =='age':
sum /= len(t) # 나이는 평균으로 계산
p_list2.append(sum) # 가중치 계산값 추가
else:
p_list2.append(t[col][idx3]) # 숫자 칼럼 아닐경우 추가
p_list2.append(t['salary'][0])
p_list.append(p_list2)
# break
cols = ['name','age','gp','gs', 'win', 'sho', 'save',
'inning', 'hits', 'ERA', 'WHIP', 'WAR', 'FIP', 'relief_WAR', 'K/9', 'BB/9', 'salary']
nego_df = pd.DataFrame(p_list, columns=cols)
연봉 예측을 위한 모델 만들기
모델 만들기
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import r2_score
import statsmodels.api as sm
def ols(X_train, y_train):
X_train = sm.add_constant(X_train)
model = sm.OLS(y_train, X_train).fit()
return model.summary()
def linearModel(df_X, df_y, showplot=True, scaler=None):
X = df_X
y = df_y
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=13, test_size=0.2)
lm =LinearRegression()
model = lm.fit(X_train, y_train)
pred_y = model.predict(X_test)
pred_train = model.predict(X_train)
mae = mean_absolute_error(y_test, pred_y)
if scaler:
mae = scaler.inverse_transform([[mae]]).round(2)
print('MAE(단위 : 만 원) : ', mae[0][0])
print("Train R2 Score : ", r2_score(y_train, pred_train))
print('Test R2 Score : ', r2_score(y_test, pred_y))
if showplot:
plt.figure(figsize=(12,6))
plt.scatter(y_test, pred_y)
plt.xlabel("True Data")
plt.ylabel("Predict Data")
plt.show()
print()
summary = ols(X_train, y_train)
print(summary)
return model
스케일러
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler
def mm_scaler(df, use_cols, getScaler=True):
mm = MinMaxScaler()
result = df[use_cols]
scaled_df = mm.fit_transform(result)
if getScaler:
return mm, pd.DataFrame(scaled_df, index=df.index, columns=use_cols)
else:
return pd.DataFrame(scaled_df, index=df.index, columns=use_cols)
def std_scaler(df, use_cols, getScaler=True):
std = StandardScaler()
result = df[use_cols]
scaled_df = std.fit_transform(result)
if getScaler:
return std, pd.DataFrame(scaled_df, index=df.index, columns=use_cols)
else:
return pd.DataFrame(scaled_df, index=df.index, columns=use_cols)
def rob_scaler(df, use_cols, getScaler=True):
rob = RobustScaler()
result = df[use_cols]
scaled_df = rob.fit_transform(result)
if getScaler:
return rob, pd.DataFrame(scaled_df, index=df.index, columns=use_cols)
else:
return pd.DataFrame(scaled_df, index=df.index, columns=use_cols)
모델 성능 평가
model = linearModel(nego_df[use_col[:-1]], nego_df[use_col[-1]])


여러 모델에 적용해보기
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.ensemble import GradientBoostingRegressor as GBR
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.decomposition import PCA
from sklearn.metrics import mean_squared_error as mse
from sklearn.metrics import r2_score as r2
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import ShuffleSplit,GridSearchCV
from sklearn.pipeline import Pipeline
use_col = ['age','gp','gs', 'win',
'inning', 'hits', 'ERA', 'WHIP', 'WAR', 'FIP', 'K/9', 'BB/9', 'salary']
cv = ShuffleSplit(n_splits=5 , test_size=0.3, random_state = 42)
mms = MinMaxScaler()
std = StandardScaler()
rob = RobustScaler()
sclaers = [mms, std, rob]
pipe_linear = Pipeline([
('fit', LinearRegression())])
pipe_gbr = Pipeline([
('fit', GBR())])
pipe_rfr = Pipeline([
('fit', RFR())])
pipe_pca = Pipeline([
('pca', PCA()),
('fit', Ridge(random_state = 42))
])
grid_params_linear = [{
"fit__fit_intercept" : [True, False],
}]
min_samples_split_range = [0.5, 0.7 , 0.9]
grid_params_gbr =[{
"fit__max_features" : ["sqrt","log2"] ,
"fit__loss" : ["ls","lad","huber","quantile"] ,
"fit__max_depth" : [5,6,7,8] ,
"fit__min_samples_split" : min_samples_split_range ,
}]
grid_params_rfr =[{
"fit__max_features" : ["sqrt","log2"] ,
"fit__max_depth" : [5,6,7,8] ,
"fit__min_samples_split" : min_samples_split_range ,
}]
grid_params_pca = [{
"pca__n_components" : np.arange(2,8)
}]
pipe = [
pipe_linear , pipe_gbr , pipe_rfr, pipe_pca
]
params = [
grid_params_linear , grid_params_gbr , grid_params_rfr, grid_params_pca
]
jobs = -1
scaler_dict = {
0 : 'MMS',
1 : 'STD',
2 : 'ROB',
3 : 'PCA'
}
grid_dict = {
0: 'Linear',
1: "GradientBoostingRegressor" ,
2: "RandomForestRegressor",
3 : 'PCA Regression'
}
model_mse = {}
model_r2 = {}
model_best_params = {}
scale_data = {}
for idx1, scaler in enumerate(sclaers):
to_scale_df = nego_df[use_col]
scaled_data = scaler.fit_transform(to_scale_df)
to_scale_df = pd.DataFrame(scaled_data, index=to_scale_df.index, columns=use_col)
X = to_scale_df[use_col[:-1]]
y = to_scale_df['salary']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=13)
isBest = 0
tmp = []
for idx2 , (param , model) in enumerate(zip(params , pipe)) :
search = GridSearchCV(model, param ,
cv=cv , n_jobs=jobs , verbose=-1 )
search.fit(X_train , y_train)
y_pred = search.predict(X_test)
r2_1 = search.score(X_train, y_train)
r2_2 = r2(y_test, y_pred)
if abs(r2_1 - r2_2) > 0.1 : continue # 테스트와 트레인 예측률에 차이가 0.1 이상이라면 패스
r_score = ((r2_1 + r2_2) / 2).round(2)
if isBest < r2(y_test, y_pred):
tmp = [idx2, mse(y_test, y_pred), r_score, search.best_params_]
isBest = r2(y_test, y_pred)
scale_data[scaler_dict.get(idx1)] = [X_train, X_test, y_train, y_test]
model_mse[scaler_dict.get(idx1) + " " + grid_dict.get(tmp[0])] = tmp[1]
model_r2[scaler_dict.get(idx1) + " " + grid_dict.get(tmp[0])] = tmp[2]
model_best_params[scaler_dict.get(idx1) + " " + grid_dict.get(tmp[0])] = tmp[3]
print("finish")
print("*"*10, ' RESULT BEST Params ', "*"*10)
for key, value in model_best_params.items():
print("{:10} : {}" .format(key, value))

(스케일러 - 모델)의 가장 좋은 모델들 시각화
fig ,ax = plt.subplots(figsize=(20, 10))
sns.set(font_scale = 2)
output = pd.DataFrame([model_r2.keys() , model_r2.values()], index = ["Scaler-Model","r2"]).T
output.sort_values(["r2"], ascending= False ,inplace=True)
ax = sns.barplot(y="Scaler-Model", x="r2", data=output)
plt.show()

타겟(최동원) 데이터 예측
gbr = GBR(loss='huber', max_depth=7, max_features='log2', min_samples_split=0.9)
target_data.sort_values('year', inplace=True)
target_scale = mms.fit_transform(target_data[use_col[:-1]])
salary_scaler = MinMaxScaler().fit(nego_df[['salary']])
model = gbr.fit(scale_data['MMS'][0], scale_data['MMS'][2])
gbr_target_pred = model.predict(target_scale)
gbr_target_pred = salary_scaler.inverse_transform(gbr_target_pred.reshape(-1,1))
print("GBR Predict")
for i in range(len(gbr_target_pred)):
print("{}년 최동원 예측 연봉 : {:.2f} 만원" .format(target_data['year'].iloc[i], gbr_target_pred[i][0]))

연봉 협상을 2년 혹은 3년마다 할 경우
# 2,3년마다 계약했을 경우
div2_target = []
div3_target = []
for i in range(1, len(target_data)):
if i % 2 == 1:
div2_target.append(target_data.iloc[[i-1, i]])
if i % 3 == 2:
div3_target.append(target_data.iloc[[i-2, i-1, i]])
ratio2_list = gravi(div2_target)
ratio3_list = gravi(div3_target)
def div23_target(df_list, ratio_list):
cols = ['name','age','gp','gs', 'win', 'sho', 'save',
'inning', 'hits', 'ERA', 'WHIP', 'WAR', 'FIP', 'relief_WAR', 'K/9', 'BB/9']
p_list = []
for idx2, t in enumerate(df_list): # 선수 개인 연봉 데이터
t = t.reset_index(drop=True)
p_list2 = []
for col in cols: #컬럼
sum = 0
flag = True # 연속형, 범주형 구분
for idx3 in range(0,len(t)): #가중치
try:
if col == 'age':
sum += t[col][idx3]
else:
sum += t[col][idx3] * ratio_list[idx2][-(idx3+1)]
except: # 컬럼이 숫자가 아니면 에러 발생
flag = False
break
if flag:
if col =='age':
sum /= len(t) # 나이는 평균으로 계산
p_list2.append(sum) # 가중치 계산값 추가
else:
p_list2.append(t[col][idx3]) # 숫자 칼럼 아닐경우 추가
p_list.append(p_list2)
# break
cols = ['name','age','gp','gs', 'win', 'sho', 'save',
'inning', 'hits', 'ERA', 'WHIP', 'WAR', 'FIP', 'relief_WAR', 'K/9', 'BB/9']
return pd.DataFrame(p_list, columns=cols)
div2_target = div23_target(div2_target, ratio2_list)
div3_target = div23_target(div3_target, ratio3_list)
div2_scale = mms.fit_transform(div2_target[use_col[:-1]])
div3_scale = mms.fit_transform(div3_target[use_col[:-1]])
div2_pred = model.predict(div2_scale)
div3_pred = model.predict(div3_scale)
div2_pred = salary_scaler.inverse_transform(div2_pred.reshape(-1,1)).tolist()
div3_pred = salary_scaler.inverse_transform(div3_pred.reshape(-1,1)).tolist()
t1 = []
t2 = []
t3 = []
for i in range(len(target_data)):
if i % 2 == 1:
t2.append(div2_pred.pop(0)[0])
else:
t2.append(0)
if i % 3 == 2:
t3.append(div3_pred.pop(0)[0])
else:
t3.append(0)
for pred1 in gbr_target_pred:
t1.append(pred1[0])
target_predict = pd.DataFrame({
'year' : target_data['year'].values,
'Nego of 1year' : t1,
'Nego of 2year' : t2,
'Nego of 3year' : t3
})
target_predict = target_predict.set_index('year').T
round(target_predict,-2).astype('int')

최동원 선수가 가장 높은 연봉을 받을 경우는 1년마다 연봉협상을 했을때 84년도에 13억으로 가장 높았다.
하지만 연봉 협상을 1년에 한 번씩 하는 경우는 드물기 때문에 3년마다 연봉 협상했을 경우 85년에 연봉 협상했다면 11억의 연봉이 책정 됐을 것으로 예측된다.
후기
머신러닝 프로젝트를 처음 진행해봤는데 생각보다 시간이 오래 걸렸다. 모델을 만들고 성능이 좋지 않아 성능 개선을 위해 고민하다 보니 데이터 수집 혹은 전처리 과정으로 돌아가는 과정을 여러번 거쳤다. 모델을 만들고 예측하는 것보다 데이터를 수집하고 전처리하는 과정에 시간이 가장 많이 소요된다라는 이야기를 많이 들었는데 뼈저리게 느낄 수 있었다.
모델 성능을 평가하는 기준을 어느정도로 잡아야하는지에 대해서도 고민이 많았다. 처음 진행한 프로젝트라 부족한 부분이 많았지만 혼자서 진행한거라 많이 배우고 얻어가는 부분이 많았다.
'데이터분석' 카테고리의 다른 글
| 온라인 게임에서의 이상 징후 탐지 기법 조사 및 분류 (0) | 2023.03.20 |
|---|---|
| Riot API 활용해서 TFT 시즌8 데이터 분석 해보기 - (1) 크롤링 (0) | 2023.03.03 |
| 따릉이 수요량 예측 (0) | 2023.01.03 |
| 스타벅스 매장은 정말 매장마다 차이가 없을까?? - (1) (0) | 2022.12.12 |
| 텍스트 마이닝 - Bag of words / TF-IDF (0) | 2022.12.12 |