728x90
반응형
크롤링
KBO 공식 홈페이지와 스탯티즈를 활용하여 크롤링을 진행하였다.
스탯티즈 크롤링
1982년부터 2023년까지 타자의 기록을 크롤링한다.
1. Import Library
from tqdm import tqdm
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import pandas as pd
- 크롤링에 사용될 selenium, pandas 등을 임포트한다.
2. get_dataframe 함수 정의
def get_dataframe(driver, team_xpath, year):
team = driver.find_element(By.XPATH, team_xpath+"button").text
try:
df = pd.read_html(driver.page_source)[1]
print(f'#######{year} {team}#######')
# 멀티 인덱스 제거
df.columns = df.columns.droplevel()
# 필요없는 칼럼 제거 WAR*(중복), 순
filter_columns = ~df.columns.duplicated()
filter_columns[0] = False
df = df.loc[:, filter_columns]
# 필요없는 열 제거
df = df[df['이름'] != '이름']
# 타입 변환 및 NULL값 제거
float_columns = ['WAR*', '타율', '출루', '장타', 'OPS', 'wOBA', 'wRC+', 'WPA']
int_columns = ['G', '타석', '타수', '득점', '안타', '2타', '3타', '홈런', '루타', '타점', '도루', '도실', '볼넷', '사구', '고4', '삼진', '병살', '희타', '희비']
df[float_columns+int_columns] = df[float_columns+int_columns].apply(pd.to_numeric, errors='coerce')
df[int_columns] = df[int_columns].astype('int')
# 소속팀 및 시즌 정보 추가
df.insert(0, '소속팀', team)
df.insert(0, '시즌', year)
df['시즌'] = df['시즌'].astype('int')
return df
except:
return pd.DataFrame()
- 예외 처리를 통해 해당 연도에 팀의 기록이 없다면 빈 데이터프레임을 반환
- 기록이 존재한다면 중복이거나 필요없는 값 제거
- 소속팀 및 시즌 정보 추가하여 데이터프레임 반환
3. main 문
if __name__ == '__main__':
df = pd.DataFrame()
# 스탯티즈 사이트 접속
url = 'http://www.statiz.co.kr/stat.php'
driver = webdriver.Chrome()
driver.get(url)
driver.implicitly_wait(10)
year_xpath = '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[1]/div/div[1]/div/'
team_xpath = '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[1]/div/div[3]/'
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[1]/div/div[9]/button'))).send_keys(Keys.ENTER)
try:
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[2]/div[2]/div[5]/form/select'))).send_keys("100")
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[2]/div[2]/div[5]/form/select/option[5]'))).click()
except:
pass
# 연도 선택 2 ~ 44
for idx1 in tqdm(range(2, 44)):
year = idx1+1980
retry = 0
max_retries = 3
while retry < max_retries:
try:
next_df = pd.DataFrame()
wait.until(EC.presence_of_element_located((By.XPATH, year_xpath+"button"))).send_keys(Keys.ENTER)
wait.until(EC.presence_of_element_located((By.XPATH,year_xpath+"ul/div/button"+f"[{idx1}]"))).send_keys(Keys.ENTER)
# 팀 선택 1 ~ 13
for idx2 in range(1, 13):
wait.until(EC.presence_of_element_located((By.XPATH, team_xpath+"button"))).send_keys(Keys.ENTER)
wait.until(EC.presence_of_element_located((By.XPATH, team_xpath+"ul/div[3]/button"+f"[{idx2}]"))).send_keys(Keys.ENTER)
next_df = pd.concat([next_df, get_dataframe(driver, team_xpath, year)])
df = pd.concat([df, next_df])
break
except:
# 사이트 재접속
url = 'http://www.statiz.co.kr/stat.php?sn=100'
driver = webdriver.Chrome()
driver.get(url)
retry += 1
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[1]/div/div[9]/button'))).send_keys(Keys.ENTER)
try:
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[2]/div[2]/div[5]/form/select'))).send_keys("100")
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div[2]/div[2]/div[5]/form/select/option[5]'))).click()
except:
pass
df.to_csv("hitter_record_1982_to_2023(ver.2).csv",index=False)
- .send_keys(100) : 출력되는 선수 수를 100으로 설정 (기존에는 30이라 누락되는 경우 방지)
- 연도별, 팀별 get_dataframe 함수 실행
KBO 크롤링
1982년부터 2023년까지 팀 순위를 크롤링한다.
1. Import Library
import pandas as pd
import numpy as np
from tqdm import tqdm
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
2. KBO 사이트 접속
# KBO 사이트 접속
url = 'https://www.koreabaseball.com/Record/TeamRank/TeamRank.aspx'
driver = webdriver.Chrome()
driver.get(url)
driver.implicitly_wait(10)
wait = WebDriverWait(driver, 10)
3.연도별 팀 순위 크롤링
columns = ['시즌', '순위', '팀명', '경기', '승', '무', '패', '승률', '게임차']
output_df = pd.DataFrame(columns=columns)
XPATH = "/html/body/form/div[3]/section/div/div/div[2]/div[2]/div[2]/select[1]"
for year in range(1982, 2024):
wait.until(EC.presence_of_element_located((By.XPATH, XPATH))).send_keys(year)
wait.until(EC.presence_of_element_located((By.XPATH, XPATH))).send_keys(Keys.ENTER)
time.sleep(1)
if year in [1999, 2000]:
df = pd.read_html(driver.page_source)[0]
df['시즌'] = year
output_df = pd.concat([output_df, df[columns]])
df = pd.read_html(driver.page_source)[1]
df['시즌'] = year
output_df = pd.concat([output_df, df[columns]])
else:
df = pd.read_html(driver.page_source)[0]
df['시즌'] = year
output_df = pd.concat([output_df, df[columns]])
output_df.to_csv("KBO_TEAM_RANK_1982_TO_2023.csv", index=False)
전처리
중복 제거
df = df[~df.duplicated()]
트레이드된 선수 처리
1. 트레이드된 선수 저장
db = []
position = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH']
for idx, row in df.iterrows():
if row['팀'][-1] in position and len(row['팀']) - 3 > 1:
db.append((row['시즌'], row['이름'],row['팀']))
elif row['팀'][-2:] in position and len(row['팀']) - 4 > 1:
db.append((row['시즌'], row['이름'],row['팀']))
db = list(set(db))
db.sort(key = lambda x : (x[0], x[1]))
2. 트레이드된 선수 데이터 삭제
for season, name, info in db:
trade_index = df[(df['시즌'] == season) & (df['이름'] == name)].index
df.drop(trade_index, inplace = True)
3. 트레이드된 선수 다시 받기
same_names = []
# 스탯티즈 사이트 접속
url = 'http://www.statiz.co.kr/stat.php'
driver = webdriver.Chrome()
driver.get(url)
driver.implicitly_wait(10)
wait = WebDriverWait(driver, 10)
trade_df = pd.DataFrame()
for year, name, info in tqdm(db):
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div/header/nav[1]/div/div[2]/form/div/input'))).send_keys(name)
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/header/nav[1]/div/div[2]/form/div/span/button'))).send_keys(Keys.ENTER)
if len(pd.read_html(driver.page_source)[0]) > 1:
same_names.append(name)
else:
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[1]/div/div[3]/div/div[2]/table/tbody/tr/td/a[2]'))).send_keys(Keys.ENTER)
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div/div[1]/div[1]/a[1]'))).send_keys(Keys.ENTER)
tmp_df = pd.read_html(driver.page_source)[1]
tmp_df.columns = tmp_df.columns.droplevel()
trade_idx = tmp_df[tmp_df['연도'] == str(year)].index[0]
tmp_df = tmp_df.iloc[[trade_idx+1, trade_idx+2]]
tmp_df = tmp_df[tmp_df['연도'].isnull()]
tmp_df.insert(0, '이름', name)
tmp_df.insert(0, '시즌', year)
trade_df = pd.concat([trade_df, tmp_df])
- 동명이인의 경우, 따로 처리해준다.
4. 트레이드된 선수 다시 받기 (동명이인)
birth = ['1967-02-22', '1965-02-10', '1971-04-17', '1973-07-27', '1971-02-28', '1969-12-18', '1972-04-09', '1973-09-02', '1973-03-24', '1980-11-12', '1971-07-14', '1973-03-24', '1970-05-29', '1979-04-19', '1980-11-12', '1979-11-15', '1986-07-10', '1980-11-12', '1984-03-21', '1985-09-04', '1987-12-26', '1986-10-21','2001-04-02']
same_db = []
idx = 0
for year, name, info in db:
if name in same_names:
same_db.append((year, name, info, birth[idx]))
idx += 1
from urllib.parse import quote
for year, name, info, birth in tqdm(same_db):
url = 'http://www.statiz.co.kr/player.php?name='+quote(name)+'&birth='+birth
driver.get(url)
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[1]/div/div[3]/div/div[2]/table/tbody/tr/td/a[2]'))).send_keys(Keys.ENTER)
wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[1]/div/section[2]/div/div[2]/div/div[1]/div[1]/a[1]'))).send_keys(Keys.ENTER)
tmp_df = pd.read_html(driver.page_source)[1]
tmp_df.columns = tmp_df.columns.droplevel()
trade_idx = tmp_df[tmp_df['연도'] == str(year)].index[0]
tmp_df = tmp_df.iloc[[trade_idx+1, trade_idx+2]]
tmp_df = tmp_df[tmp_df['연도'].isnull()]
tmp_df.insert(0, '이름', name)
tmp_df.insert(0, '시즌', year)
trade_df = pd.concat([trade_df, tmp_df])
5. 추가 후처리
- 소속팀에 통합팀(ex. 두산+)으로 되어 있어서 연도에 맞게 소속팀 수정(OB 베어스, 두산 베어스)
- 포지션 칼럼생성해서 포지션 저장
- 각 데이터프레임 합치기
6. 트레이드 선수 확인
시각화
태블로를 활용하여 다음과 같이 완성했다.
- Year, Top N 파라미터를 변경하면 관련 그래프가 변화한다.
- Year을 변경하면 좌측은 해당 그래프까지, 가운데는 해당 연도의 팀순위, 우측은 해당 연도의 주요 타격 순위
- Top N을 변경하면 우측 그래프에서 N까지 확인 가능
- 팀 혹은 선수를 클릭하면 그래프가 변화한다.
- 팀 클릭 시: 왼쪽 그래프와 오른쪽 그래프의 대상이 해당 팀으로 바뀐다.
- 선수 클릭 시: 왼쪽 그래프의 대상이 해당 선수로 바뀐다.
- 팀 and 선수 클릭 시: 선수가 해당 팀에서 뛰었다면 해당 팀에서 뛴 기간만 나타난다. 뛰지 않았다면 아무것도 나타나지 않는다. (
예외처리 해야하는데...)
- 우측 그래프에서 타율, 출루율은 규정타석 채운 타자만 나타나도록 했다.
간단 분석
2023 롯데의 간단 분석을 해본다.
1. 2023 롯데
- 타율은 비슷한데, 오히려 출루율은 1푼이나 올랐다.
- 하지만, 홈런 수가 급감했다. (34.91% 감소)
- 공격이 답답했다고 느낀 이유인듯 하다. (
성멘...)
- 공격이 답답했다고 느낀 이유인듯 하다. (
- 규정타석 채운 선수가 3명밖에 안된다. (
실화???)
2. 롯데 안치홍
안치홍 선수가 한화로 가버렸다. 아쉽지만, 샐러리 캡때문에... 가서 잘하시길... 롯데 시절의 안치홍을 살펴보자.
- 사실 클래식한 지표로는 잘했다, 못했다 하기 어려운 듯하다. (WAR은 가져와야 될 듯)
- 작년에는 타율이나 출루율이 FA기간중에 낮은 편이었지만, 홈런이 많았다.
- 근데, 안타가 젤 많았으니까 그만큼 타수가 많았다는 뜻이겠다. (찾아보니 70타수정도 차이나더라.)
결과
728x90
반응형
'프로젝트' 카테고리의 다른 글
ChatGPT에게 야구 지식 가르쳐주기 (1) - LangChain을 활용하여 RAG 구성하기 (0) | 2023.12.23 |
---|---|
2023 KBO 선발투수 HEATMAP 시각화 프로젝트 (2) - 시각화 (2) | 2023.11.20 |
2023 KBO 선발투수 HEATMAP 시각화 프로젝트 (1) - 데이터 크롤링 및 전처리 (1) | 2023.11.14 |
[프로젝트] numpy, pandas로 신경망 구현하여 프로야구 순위 예측하기 (0) | 2022.07.19 |