머신러닝과 기상 데이터를 이용해 홈런 갯수 예측해보기 (1) 소스 가져오기

    목차
반응형

야구가 기상의 영향을 받는다는 사실은 잘 알려져있다.

그 중에서도 기온이나 습도, 풍향/풍속 등에 가장 영향을 많이 받을 것이다.

그렇다면 이런 기상 정보와 홈런의 갯수에는 관계가 있지 않을까 하는 생각에서부터 이 머신러닝 만들기를 시작했다.

 

 

 

 

 

앞 부분은 전에 머신러닝으로 득/실점을 예측할 때랑 거의 같다.

한가지 다른 점은 한 팀만의 경우가 아닌 모든 경기의 결과를 수집해야한다는 점이다.

import urllib.request
from bs4 import BeautifulSoup
import pandas as pd
import os

# url 가져오기
def geturl(date, teamname):
    (year, month) = ym(date)
    url = "https://sports.news.naver.com/kbaseball/schedule/index.nhn?&month="+str(month)+"&year="+str(year)+"&teamCode="+teamname
    return url


# 날짜 자르기
def ym(date):
    year = str(date)[:4]
    month = str(date)[-2:]
    return (year, month)

# 앞뒤 간편하게 자르기
def cuturl(url):
    req = urllib.request.Request(url)
    html = urllib.request.urlopen(req).read()
    soup = BeautifulSoup(html, "lxml")
    st = soup.get_text()

    cut1 = st.find("알림받기")
    cut2 = st.find("야구 홈으로")
    st = st[cut1+4:cut2]
    st = st.strip()

    return st

 

득/실점 데이터를 모을 때는 상대팀과 득/실점이 중요했다면 이제는 원정팀, 홈팀, 구장이 중요한 데이터가 된다.

롯데 경기를 예를 들었을 때, 대부분 사직에서 열리긴 하지만 울산이나 NC 창단 이전에는 마산구장에서 경기를 펼치기도 했다. 따라서 롯데라고 단순하게 사직에서 열렸다고 하면 오류가 생기므로 구장의 정보를 뽑아낼 필요가 생겼다.

 

# 각 날짜별로 나누기
def cutst(st):
    teamlist = ["SK ", "SK\n", "삼성", "LG ","LG\n", "롯데", "KIA", "한화", "넥센", "키움", "KT", "NC", "두산", "kt"]
    tmp = []

    while ("SK "in st) or ("SK\n" in st) or ("삼성" in st) or ("LG " in st) or ("롯데" in st) or ("KIA" in st) \
            or ("한화" in st) or ("넥센" in st) or ("키움" in st) or ("KT" in st) or ("kt" in st) \
            or ("NC" in st) or ("두산" in st) or ("LG\n" in st) :
        tmp2 = {}
        tmp3 = []
        for team in teamlist:
            pos = st.find(team)
            if pos == -1 :
                continue
            else :
                tmp2[team] = pos

        for k, v in tmp2.items():
            tmp3.append((v, k))
            tmp3 = sorted(tmp3)
        if len(tmp3) == 0 :
            break

        pos1 = tmp3[0][0]
        pos2 = tmp3[1][0]
        
        st1 = st[:pos1].strip()
        st2 = st[pos1:pos2+4].strip()
        
        st1 = finddate(st1)

        st3 = st[pos2+4:]

        co = st3.find(":")
        dot = st3.find(".")

        if (co < 0) and (dot>0) :
            pos3 = dot
            st3 = st3[:pos3-5].strip()
            st3 = st3[-4:].strip()
        elif (co < 0) and (dot<0) :
            st3 = st3[-4:].strip()
        elif (co > 0 ) and (dot < 0) :
            pos3 = co
            st3 = st3[:pos3-5].strip()
            st3 = st3[-4:].strip()
        else :
            pos3 = min(co, dot)
            st3 = st3[:pos3-5].strip()
            st3 = st3[-4:].strip()

        
        if "(LG)" in st3:
            st3 = st3[:2]

        pos4 = st2.find("SKY")
        if pos4 > 0 :
            st2 = st2[pos4+10:].strip()
        
        tmp.append((st1, st2, st3))
        st = st[pos2+3:].strip()

        sky = st.find("SKY")

        if (sky > 0 ) and (sky < 30):
            st = st[sky+10:].strip()
            
    return tmp

# string에서 날짜 찾기
def finddate(st):
    pos1 = st.find("없습니다")
    pos2 = st.find("V2")
    pos3 = st.find("2TV")
    if (pos1 >= 0) or (pos2 >= 0) or (pos3 >= 0):
        pos = max(pos1, pos2, pos3)
        st = st[pos+5:].strip()
        return finddate(st)
    try :
        if len(st) == 0 :
            return st
        x = int(st[0])
        return st
    except :
        st = st[1:]
        return finddate(st)

좀 많이 복잡한 것 같지만 전에 쓰던 코드에서 구장 정보를 뽑도록 추가했을 뿐이다.

하루에 보통 4~5경기가 펼쳐지는데 소스를 불러와서 보게 되면 날짜나 경기시작시간 앞에 구장이 적혀있는 것을 확인할 수 있다. 날짜에는 ., 경기시작시간에는 :가 있다는 점을 이용해서 그 앞을 잘라 구장을 찾아줬다.

다만 LG의 경우 시범경기 중 이천(LG)라는 곳이 있어서 (LG)부분을 없애줬다.

이제 cutst()를 해주면 [('날짜 경기시간', '원정 스코어:스코어 홈', '구장'), ...]가 반환이 된다.

 

 

# 날짜/홈팀/상대팀으로 정리하기
def score(ls):
    tmp = []
    i = -1
    dtmp = []
    
    for datestring, teamscore, stadium in ls :
        pos0 = datestring.find(".")
        pos1 = datestring.find("(")
        pos2 = datestring.find(")")

        month = datestring[:pos0].strip()
        date = datestring[pos0+1:pos1].strip()

        try :
            month = int(month)
            date = int(date)
            dtmp.append((month, date))
            i = i + 1
            sameday = len(tmp)
        except :
            month = dtmp[i][0]
            date = dtmp[i][1]


        if "VS" in teamscore:
            continue
        else :
            away = teamscore[:3].strip()
            home = teamscore[-3:].strip()

        # 더블헤더 체크
        double = 0
        if len(tmp) > sameday :
            for (month2, date2, away2, home2, double2, stadium2) in tmp[sameday:]:
                if away2 == away:
                    double = 1
                    break
            
        tmp.append((month, date, away, home, double, stadium))
    return tmp

저번 롯데경기만의 데이터를 뽑아낼 때는 더블헤더가 아닌 이상 하루에 한 경기만 했기 때문에 경기 스코어 앞에 항상 날짜 정보가 있었다.

하지만 모든 경기로 확인하면 하루 중에 맨 위의 경기 앞에만 날짜 정보가 있고 다음 3~4경기는 경기 시간 정보만 있다는 문제점이 생긴다. 그걸 없애기 위해 dtmp 리스트에 첫 경기의 날짜 정보를 추가하고 다음 경기부터는 dtmp의 날짜 정보를 가져오게 했다.

그리고 일반적인 경기나 더블헤더 첫 경기의 경우에는 double = 0, 더블헤더 두 번째 경기의 경우 double = 1을 받았다.

 

 

 

이렇게 하면 날짜 정보와 원정팀, 홈팀, 더블헤더 유무를 반환하게 되는데 이 데이터는 홈런 데이터를 뽑아내는데 필요한 부수적인 것들이다. (여태껏 메인이 아니었다.)

네이버 스포츠 기록실에서 각 경기별 경기결과를 보게 되면

https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=20200508SKLT02020&category=kbo

이런 형태로 되어있다.

gameId를 자세히 살펴보면 "년월일"+"원정팀"+"홈팀"+"더블헤더 유무"+"연도" 순으로 되어있다.

특히 자세히 봐야할 것은 더블헤더 유무 부분인데, 더블헤더가 아니면 0, 더블헤더 1경기는 1, 더블헤더 2경기는 2로 되어있다. 그렇기 때문에 홈런 데이터를 뽑아내기 위해선 더블헤더 유무가 굉장히 중요했기 때문에 위 코드에서 더블헤더 유무를 반드시 반환해내야했다.

 

그뿐만이 아니라 2008~2015년의 자료를 보면

https://sports.news.naver.com/gameCenter/gameResult.nhn?category=kbo&gameId=20150501LTHH0

가장 뒤에 있어야할 연도인 2015가 없다. 만약 2015를 적으면 잘못된 접근 경로라고 뜬다. 따라서 이것도 잘 처리해야했다.

 

또 포스트 시즌 경기의 경우, 날짜 앞의 연도에 와카전은 4444, 준플은 3333, 플옵은 5555, 한국시리즈는 7777로 되어있으므로 그것도 유의해야했다.

 

import requests

# 경기 상세 기록 url 가져오기
def geturl2(year, month, date, away, home, double) :
    
    if int(year) <= 2015 :
        url = "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                +str(year)+str(month)+str(date)+away+home+"0"+"&category=kbo"
        req = requests.get(url)
        html = req.text

        if "잘못된 접근" in html :
            if double == 0 :
                url = "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                        +str(year)+str(month)+str(date)+away+home+"1"+"&category=kbo"
            else :
                url = "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                        +str(year)+str(month)+str(date)+away+home+"2"+"&category=kbo"

    else :
        url = "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                +str(year)+str(month)+str(date)+away+home+"0"+str(year)+"&category=kbo"

        req = requests.get(url)
        html = req.text
        
        if "잘못된 접근" in html :
            if double == 0 :
                url = "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                        +str(year)+str(month)+str(date)+away+home+"1"+str(year)+"&category=kbo"
            else :
                url = "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                        +str(year)+str(month)+str(date)+away+home+"2"+str(year)+"&category=kbo"


    #가을야구
    req = requests.get(url)
    html =req.text
    postseason = [3333, 4444, 5555, 7777]
    for i in postseason :
        if int(year) <=2015 :
            url2 =  "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                    +str(i)+str(month)+str(date)+away+home+"0"+"&category=kbo"
        else :
            url2 = "https://sports.news.naver.com/gameCenter/gameRecord.nhn?gameId=" \
                    +str(i)+str(month)+str(date)+away+home+"0"+str(year)+"&category=kbo"
            
        req = requests.get(url2)
        html = req.text
        if "잘못된 접근" not in html :
            url = url2
            break
        
    return url

우선적으로 더블헤더가 아니라고 가정하에 소스를 불러와서 만약 잘못된 접근이 아니라면 0,

잘못된 접근이라면 더블헤더 경기이므로 double 값을 체크해서 0이면 첫경기이므로 1, 아니면 2,

가을야구일 수도 있으니 마지막으로 체크하는 방식을 거쳤다.

이렇게 하면 시간이 좀 걸린다는 단점이 있을 수도 있으나 수작업으로 일일이 처리하는 것보단 빠르다고 생각한다.

 

 

 

(다음 글에 이어서)

728x90
반응형