2019년 프로토 결산으로 보는 정배 vs 역배
Intro
작년 12월 이었습니다. 커뮤니티나 기술뉴스, 블로그 등에서 연말을 맞아 2019년 회고, 결산 등을 올리는 한 해를 마무리하는 시기였습니다. 이것저것 보다 한 사이트의 2019년 결산 인포그래픽
을 보고 나도 어떤 것에 2019년 결산을 다루어보면 재밌지 않을까?
라는 생각에 글을 쓰기 시작했습니다.
주제를 고민하다 야구를 좋아하니 스포츠 관련 데이터를 모으기로 방향을 정했고, 야구 토토 데이터를 모았습니다. 하지만 무언가 결산이라고 부를만 하기에는.. 양이 풍부하지않아 글을 어느정도 쓰다 다른 데이터를 찾아보기로 했습니다.
고민하다 토토 종목중에 프로토 승부식을 발견했고 게임 수가 많아 데이터를 모아보면 무언가 있겠지?..
라는 심정으로 주제를 정했습니다. 프로토 승부식은 흔히 토토하면 생각나는? 승무패를 맞추는 종목입니다.
데이터 크롤링
먼저 제가 재미있겠다 싶었던 데이터는 베트맨에서 제공하는 1page1game
페이지였습니다. 게임구매 목록에서 각 경기별로 1page1game
을 제공합니다. 여기서 사람들이 어떤 쪽에 구매를 많이 했는지, 승무패 별로 배당률이 얼마인지를 알 수 있습니다.
아래 그림은 2019년 12월 31일에 열린 워싱턴과 마이애미의 농구경기인데 구매 투표율을 보면 워싱턴이 패(74%)한다는 쪽으로 많이 기울었네요.
(참고로 워싱턴이 이겼습니다)
Figure 1. 1page1game 페이지 예시
text/html packet을 가져오자!
데이터 크롤링을 한다고하면 puppeteer나 phantomjs같은 headless browser
를 사용하여 html을 가져오는 것을 생각합니다. 그런데 당시 생각으로 베트맨사이트가 https
가 아닌 http
인것을 보고 packet을 가져오는 것도 괜찮지 않을까? 생각했습니다. 그래서 각 경기별로 1page1game
페이지를 열면서 제 컴퓨터로 들어오는 text/html 관련 packet을 wireshark로 기록하면 되겠다는 생각을 합니다.
2019년 전체에 대해서 하려고하니 경기수가 상당히 많았습니다. 그래서 각 경기별로 1page1game
을 요청하는 스크립트를 작성했습니다. 스크립트는 devTool console에 넣어 실행시켰습니다. 코드는 대략 아래와 같습니다.
// script for opening 1page1game
async function do_run(gset) {
const loop = async function loop() {
const promise = new Promise(function (resolve, reject) {
setTimeout(function() {
resolve(true);
}, 5000);
});
return promise;
};
const beepSound = async function makeBeep() {
const beep = async function beep() {
const promise = new Promise(function (resolve, reject) {
setTimeout(function() {
const sound = new Audio("audio data here!");
sound.play();
resolve(true);
}, 1000);
});
return promise;
};
await beep();
await beep();
await beep();
}
// gset is set of game list
for (const [index, g] of Object.entries(gset)) {
const gameNumber = g[0];
const gameType = g[1];
// inner javascript function
oneGameOnePageWinOpen(gameNumber,gameType);
await loop();
}
await beepSound();
console.log('processing done!');
};
경기 리스트 gset을 순회하며 베트맨 내부 함수인 oneGameOnePageWinOpen
을 호출하고 완료 되면 비프음을 내도록 했습니다. 1page1game
로드 시간을 고려하여 넉넉하게 매 5초마다 다음 페이지를 요청하도록 했습니다. wireshark
로 기록된 packet도 쉽게? 추출 할 수 있어 처음에는 신나게 스크립트를 돌렸습니다. 하지만 얼마지나지 않아 중요한 사실을 깨달았습니다.
http request를 날리는 스크립트를 작성하자!
2019년 프로토 승부식
은 총 103라운드가 있습니다. 한 라운드당 대충 100 경기씩 있다고 했을때 위와 같은 방법으로는 데이터를 모으는데 시간이 많이 걸립니다. 한 경기당 못해도 5~6초씩은 걸려서 데이터를 모으고 있으니 대략 17시간 동안 쉬지않고 해야 합니다. 이건 뭔가 아니다 싶어 더 빠른 방법을 찾기 시작했습니다.
생각해보니 packet을 그대로 가져와도 원하는 정보가 있다는 말은 서버에서 html file을 static하게 만들어주고 있다는 것을 의미합니다. 그렇다면 브라우저가 1page1game
을 요청하는 http request를 똑같이 만들어서 nodejs로 스크립트를 돌리면 되지 않을까요? devTools에 network 탭을 자세히 보며 postman으로 http reqeust를 날려 가능성을 본 후 아래의 코드를 작성했습니다.
// script for making http reqeust
const request = require('request');
const iconv = require('iconv-lite');
const fs = require('fs');
function makeRequestAndSave(filename, option) {
const promise = new Promise(function(resolve, reject) {
request(option, function(error, response, body) {
const data = iconv.decode(body, 'euc-kr');
const saveDone = writeFile(filename, data);
if (saveDone) {
resolve(true)
}
})
});
return promise;
}
npm에서 흔히 쓰는 package인 request로 http request 만들어서 보냅니다. option
은 http request header에 들어가는 옵션으로 아래와 같은 형태입니다. 1page1game
은 로그인한 사용자에게만 보이는 페이지이기 때문에 Cookie값에 인증 정보를 담아 줍니다.
// option example
const optionExample = {
"url": "http://betman.co.kr/oneGame.so",
"method": "POST",
"headers": {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Your User-Agent",
"Origin": "http://betman.co.kr",
"Referer": `http://betman.co.kr/gameSchedule.so?method=basic&gameId=${gameId}&gameRound=${gameRound}`,
"Cookie": "Your Cookie",
"Connection": "keep-alive",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.9,ko;q=0.8,de;q=0.7,ko-KR;q=0.6",
"Cache-Control": "max-age=0",
},
"form": {
"GM_ID": gameId,
"GM_RND": gameRound,
"GM_SEQ": gameSeq,
"UP_DT": "202001040549",
"GM_TYPE": null,
},
"encoding": null,
"qs": {
"method": "getOneGameMain",
"GM_TYPE": gameType,
}
}
처음에는 response로 받은 파일을 열어보니 문자들이 깨져 읽을 수 없어 iconv-lite로 알맞은 charset으로 decoding을 해주어 저장합니다. 간단하게 request를 보내고 받기만 하면 되어 한 경기당 1초면 충분했습니다. 17시간 걸릴 것을 2.8시간으로 줄여 html file을 모았습니다.
Raw data 가공
이제 분석하기 쉬운 형태로 html file을 잘 가공해야합니다. 원하는 부분만 추출하기 위해 각 html file에 DOM
을 만듭니다. javascript에서는 jsdom을 많이쓰는 것 같아 jsdom
으로 DOM
을 만든 후, querySelector를 이용해 필요한 데이터를 csv
형태로 저장합니다. 코드는 아래와 같습니다. 워싱턴과 마이애미의 농구경기는 6,워싱턴W,마이애H,26%,null,74%,3.50,null,1.14,01
이 되겠네요
const fs = require('fs');
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
function doSomething(dirPath, filename) {
try {
const html = fs.readFileSync(`${dirPath}/${filename}`, 'utf-8');
const dom = new JSDOM(html);
const gameSeq = dom.window.document.querySelector("input[name=GM_SEQ]").value;
const queries = [
'#sr_home > h3',
'#sr_away > h3',
'#status_home_win_bar > strong',
'#status_home_draw_bar > strong',
'#status_home_lose_bar > strong',
'#current_win',
'#current_draw',
'#current_lose',
];
const parsingResults = queries.map(query => {
const element = dom.window.document.querySelector(query);
if (!element) {
return 'null';
}
return element && element.textContent.trim();
});
return [gameSeq, ...parsingResults].join(',');
} catch(err) {
console.log(`err at ${filename}, ${err.message}`);
return null;
}
}
// return example
// gameNumber, homeTeam, awayTeam, homew win, home draw, home lose, win odd, draw odd, lose odd, gameResult`
`6,워싱턴W,마이애H,26%,null,74%,3.50,null,1.14,01`
데이터 보기
이제 원하는 것을 얻었으니 결산을 내봅시다!
베트맨에서
프로토 승부식
종목에1page1game
을 제공하는 경기에 대한 결산입니다.
- 2019년 총
103라운드
동안 약11,200경기
가 진행되었습니다. 그 중 약9,600경기
에서1page1game
을 제공합니다. - 스포츠 종목별로
9,600경기
에서야구 3429경기(36%)
,축구 3016경기(31%)
,농구 2503경기(26%)
,배구 667경기(7%)
가 진행되었습니다. - 정배를 구매 투표가 많은쪽, 역배를 그 나머지라고 했을 때 적중률은 54.6% 입니다. 남들이 가는 쪽에 가기만해도 100번중 54번은 맞다는 의미죠 (취소된 경기는 제외했습니다)
- 스포츠 종목별로 적중률은 각각
농구 61%
,야구 58%
,배구 56%
,축구 45%
입니다. 축구가 가장 이변이 많은 종목이네요. - 가장 구매율이 많이 몰린 경기는 2019년 4월 10일 뉴욕 메츠 vs 미네소타의 야구경기입니다. 무려 97%! 로 뉴욕 메츠의 승리를 예측했습니다. 선발대결이 디그롬 vs 깁슨인 경기였군요.
참고로 이 경기는 미네소타가 이깁니다..
- 가장 배당률이 높은경기는 2019년 4월 14일 PSV vs 데그라프의 축구경기입니다. 데그라프가 이기는 쪽에 배당률이
26배
였네요. 이 경기는 PSV가 이깁니다
아래는 각 라운드별 적중률입니다.
Figure 2. gameRound별 구매 예측이 맞은 비율
정배 vs 역배
그렇다면 매 경기별로 100원
씩 구매한다고 할 때 정배, 역배에서 각각 얼마나 이득을 보고 손해를 볼까요? 취소된 경기를 제외한 9,352경기의 배팅 결과는 다음과 같습니다.
-
정배에만 걸었을 때에는 935,200원을 사용해 827,800원을 얻습니다. 107,400원(11%) 손해를 보네요
-
역배에만 걸었을 때에는 935,200원을 사용해 781,200원을 얻습니다. 154,000원(16%) 손해를 보네요
승패만 있는 경기는 정배, 역배만 있지만 승무패가 있는 경우에는 역배가 2개가 있습니다. 이 경우는 구매율이 가장 낮은 것을 역배로 하였습니다.
경기 종목별 정배, 역배 결과는 아래와 같습니다. (이득 / 구매, 손해비율)
- 정배 :
야구(2888/3245, 11%)
,축구(2597/2996, 13%)
,농구(2208/2456, 10%)
,배구(584/655, 11%)
- 역배 :
야구(2731/3245, 16%)
,축구(2557/2996, 15%)
,농구(1971/2456, 20%)
,배구(551/655, 16%)
어떤 전략을 취하든 대중의 흐름만을 보고 구매를 하는 경우에는 손해를 볼 수 밖에 없다는 것을 알 수 있네요.
Outro
이번 글을 쓰며 인터넷에서 보는 정보를 가져와서 원하는대로 다루는 것은 생각보다 손이 많이 가는 작업이라는 것을 느꼈습니다. 특히 예상치 못한 부분에서 삽질을 하기도 했습니다. (euc-kr encoding, http request option, dom node 다루기 등등) 마지막으로 느낀점을 나열하며 글을 마칩니다.
- http reqeust를 더 잘 이해하는 계기가 되었다
- 베트맨을 보며 사이트가 어떤 방식으로 짜여져있는지 구조를 역으로 이해해보는 연습을 했다
- 정배든 역배든 손해만 본다