Skip to content

Commit 0bbaa00

Browse files
committed
Add predict, rank, standings commands
1 parent ab2cb5e commit 0bbaa00

File tree

7 files changed

+838
-52
lines changed

7 files changed

+838
-52
lines changed

commands/create.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = async (argv) => {
1818
db.run(`DROP TABLE IF EXISTS teams`),
1919
db.run(`DROP TABLE IF EXISTS matches`),
2020
db.run(`DROP TABLE IF EXISTS competitions`),
21+
db.run(`DROP TABLE IF EXISTS rankings`),
2122
]);
2223
await Promise.all([
2324
db.run(`CREATE TABLE teams (
@@ -44,5 +45,11 @@ module.exports = async (argv) => {
4445
FOREIGN KEY(awayteam) REFERENCES teams(teamid),
4546
FOREIGN KEY(matchcompetition) REFERENCES competitions(competitionid)
4647
);`).then(() => db.run(`CREATE UNIQUE INDEX idx_uniquematches ON matches(hometeam, awayteam, date);`)),
48+
db.run(`CREATE TABLE rankings (
49+
rankingteamid INTEGER NOT NULL,
50+
elo INTEGER NOT NULL,
51+
date INTEGER NOT NULL,
52+
FOREIGN KEY(rankingteamid) REFERENCES teams(teamid)
53+
);`).then(() => db.run(`CREATE UNIQUE INDEX idx_uniquerankings ON rankings(rankingteamid, date);`)),
4754
]);
4855
}

commands/predict.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const db = require('sqlite');
2+
const fs = require('fs-extra');
3+
const teams = require('../data/teams');
4+
const results = require('../data/results');
5+
6+
module.exports = async (argv) => {
7+
const {dbPath, verbose, _} = argv;
8+
if (verbose) {
9+
console.info(`using db in ${dbPath}`);
10+
}
11+
12+
await db.open(dbPath, { Promise });
13+
14+
// pop the command name
15+
_.shift();
16+
let hometeam, awayteam;
17+
if (_[1] === 'at') {
18+
hometeam = _[2];
19+
awayteam = _[0];
20+
} else if (_[1] === 'vs') {
21+
hometeam = _[0];
22+
awayteam = _[2];
23+
} else {
24+
throw new Error(`Unrecognized conjunction ${_[1]}; please use "at" or "vs"`);
25+
}
26+
27+
const homeTeamStanding = await db.get(`
28+
SELECT teamname, elo
29+
FROM rankings
30+
INNER JOIN teams on teams.teamid = rankings.rankingteamid
31+
WHERE date = (
32+
SELECT max(date)
33+
FROM rankings
34+
WHERE rankings.rankingteamid = teams.teamid
35+
)
36+
AND (teams.abbreviation = "${hometeam}" OR teams.teamname = "${hometeam}")
37+
ORDER BY elo DESC
38+
`);
39+
40+
const awayTeamStanding = await db.get(`
41+
SELECT teamname, elo
42+
FROM rankings
43+
INNER JOIN teams on teams.teamid = rankings.rankingteamid
44+
WHERE date = (
45+
SELECT max(date)
46+
FROM rankings
47+
WHERE rankings.rankingteamid = teams.teamid
48+
)
49+
AND (teams.abbreviation = "${awayteam}" OR teams.teamname = "${awayteam}")
50+
ORDER BY elo DESC
51+
`);
52+
53+
if ((homeTeamStanding.elo + 100) > awayTeamStanding.elo) {
54+
console.log(`I predict ${homeTeamStanding.teamname} will win.`);
55+
} else {
56+
console.log(`I predict ${awayTeamStanding.teamname} will win.`)
57+
}
58+
}

commands/rank.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
const db = require('sqlite');
2+
const fs = require('fs-extra');
3+
const teams = require('../data/teams');
4+
const results = require('../data/results');
5+
6+
module.exports = async (argv) => {
7+
const {dbPath, verbose} = argv;
8+
if (verbose) {
9+
console.info(`using db in ${dbPath}`);
10+
}
11+
12+
await db.open(dbPath, { Promise });
13+
14+
const startDate = new Date('December 11, 1973').getTime();
15+
const startElo = 1300;
16+
console.log(`seeding table with start elo ${startElo} on date ${startDate}`);
17+
await Promise.all(teams.map(team => db.run(`
18+
INSERT INTO rankings (rankingteamid, elo, date) VALUES (
19+
(SELECT teamid FROM teams WHERE teams.abbreviation = "${team.abbreviation}"),
20+
${startElo},
21+
${startDate}
22+
)
23+
`)));
24+
const matches = await db.all(`
25+
select
26+
competitions.k,
27+
hometeam,
28+
awayteam,
29+
homegoals,
30+
awaygoals,
31+
matches.date as date
32+
from matches
33+
inner join competitions on competitions.competitionid = matches.matchcompetition
34+
order by date ASC
35+
`);
36+
console.log(`processing ${matches.length} matches`);
37+
for (const match of matches) {
38+
const awayranking = await db.get(`
39+
select
40+
rankings.elo,
41+
rankings.date
42+
from rankings
43+
inner join (
44+
select
45+
max(date) as date,
46+
max.rankingteamid
47+
from rankings as max
48+
where max.rankingteamid = ${match.awayteam}
49+
group by max.rankingteamid
50+
) as maxranking
51+
on rankings.rankingteamid = ${match.awayteam}
52+
and rankings.date = maxranking.date
53+
`);
54+
const homeranking = await db.get(`
55+
select
56+
rankings.elo,
57+
rankings.date
58+
from rankings
59+
inner join (
60+
select
61+
max(date) as date,
62+
max.rankingteamid
63+
from rankings as max
64+
where max.rankingteamid = ${match.hometeam}
65+
group by max.rankingteamid
66+
) as maxranking
67+
on rankings.rankingteamid = ${match.hometeam}
68+
and rankings.date = maxranking.date
69+
`);
70+
match.homeranking = homeranking.elo;
71+
match.awayranking = awayranking.elo;
72+
let homeElo, awayElo;
73+
if (match.homegoals < match.awaygoals) {
74+
homeElo = updateElo({
75+
match,
76+
won: false,
77+
isHome: true
78+
});
79+
awayElo = updateElo({
80+
match,
81+
won: true,
82+
isHome: false
83+
});
84+
} else if (match.homegoals === match.awaygoals) {
85+
homeElo = updateElo({
86+
match,
87+
won: false,
88+
isHome: true
89+
});
90+
awayElo = updateElo({
91+
match,
92+
won: false,
93+
isHome: false
94+
});
95+
} else {
96+
homeElo = updateElo({
97+
match,
98+
won: true,
99+
isHome: true
100+
});
101+
awayElo = updateElo({
102+
match,
103+
won: false,
104+
isHome: false
105+
});
106+
}
107+
await Promise.all([db.run(`
108+
INSERT INTO rankings (elo, date, rankingteamid) VALUES (
109+
${homeElo},
110+
${match.date},
111+
${match.hometeam}
112+
)
113+
`),
114+
db.run(`
115+
INSERT INTO rankings (elo, date, rankingteamid) VALUES (
116+
${awayElo},
117+
${match.date},
118+
${match.awayteam}
119+
)
120+
`)
121+
]);
122+
console.log(`finished processing ${match.hometeam} vs ${match.awayteam} on ${match.date}`);
123+
}
124+
}
125+
126+
function updateElo({match, isHome, won}) {
127+
console.log(`${JSON.stringify(match)} ${isHome} ${won}`);
128+
let priorElo, opponentPriorElo;
129+
if (isHome) {
130+
priorElo = match.homeranking;
131+
opponentPriorElo = match.awayranking;
132+
} else {
133+
priorElo = match.awayranking;
134+
opponentPriorElo = match.homeranking;
135+
}
136+
const k = match.k;
137+
console.log(`priorElo ${priorElo} opponentPriorElo ${opponentPriorElo} k ${k}`);
138+
139+
const eloDifference = priorElo - opponentPriorElo;
140+
let homeAdjustedEloDifference;
141+
if (isHome) {
142+
homeAdjustedEloDifference = eloDifference + 100;
143+
} else {
144+
homeAdjustedEloDifference = eloDifference - 100;
145+
}
146+
const x = homeAdjustedEloDifference / 200;
147+
const sexp = 1 / (1 + Math.pow(10, -x/2));
148+
console.log(`eloDifference ${eloDifference} x ${x} sexp ${sexp}`);
149+
150+
let sact;
151+
if (won) {
152+
let losingGoals = isHome ? match.awaygoals : match.homegoals;
153+
if (losingGoals > 5) {
154+
losingGoals = 5;
155+
}
156+
let goalDifferential = Math.abs(match.homegoals - match.awaygoals);
157+
if (goalDifferential > 6) {
158+
goalDifferential = 6;
159+
}
160+
sact = (1 - (results[losingGoals][goalDifferential] * .01));
161+
} else {
162+
let losingGoals = isHome ? match.homegoals : match.awaygoals;
163+
if (losingGoals > 5) {
164+
losingGoals = 5;
165+
}
166+
let goalDifferential = Math.abs(match.homegoals - match.awaygoals);
167+
if (goalDifferential > 6) {
168+
goalDifferential = 6;
169+
}
170+
sact = (results[losingGoals][goalDifferential] * .01);
171+
}
172+
console.log(`sact ${sact} returning ${priorElo + (k * (sact - sexp))}`);
173+
return priorElo + (k * (sact - sexp));
174+
}

commands/standings.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const db = require('sqlite');
2+
const fs = require('fs-extra');
3+
const teams = require('../data/teams');
4+
const results = require('../data/results');
5+
6+
module.exports = async (argv) => {
7+
const {dbPath, verbose} = argv;
8+
if (verbose) {
9+
console.info(`using db in ${dbPath}`);
10+
}
11+
12+
await db.open(dbPath, { Promise });
13+
14+
const dateResult = await db.get(`
15+
SELECT max(date) as date
16+
FROM rankings
17+
`);
18+
const standingsDate = new Date(dateResult.date);
19+
20+
const standings = await db.all(`
21+
SELECT teamname, elo
22+
FROM rankings
23+
INNER JOIN teams on teams.teamid = rankings.rankingteamid
24+
WHERE date = (
25+
SELECT max(date)
26+
FROM rankings
27+
WHERE rankings.rankingteamid = teams.teamid
28+
)
29+
AND teams.abbreviation NOT IN ('MIA', 'CHV', 'TBM')
30+
ORDER BY elo DESC
31+
`);
32+
33+
console.log(`Standings as of ${standingsDate.toLocaleDateString()}\n`);
34+
standings.forEach((standing, i) => {
35+
console.log(`${i + 1}.) ${standing.teamname} (${Math.round(standing.elo)})`);
36+
});
37+
}

data/results.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = [
2+
[47, 15, 8, 4, 3, 2, 1],
3+
[50, 16, 8.9, 4.8, 3.7, 2.6, 1.5],
4+
[51, 17, 9.8, 5.6, 4.4, 3.2, 2],
5+
[52, 18, 10.7, 6.4, 5.1, 3.8, 2.5],
6+
[52.5, 19, 11.6, 7.2, 5.8, 4.4, 3],
7+
[53, 20, 12.5, 8, 6.5, 5, 3.5],
8+
];

index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
const dataCommand = require('./commands/data');
33
const createCommand = require('./commands/create');
44
const importCommand = require('./commands/import');
5+
const rankCommand = require('./commands/rank');
6+
const standingsCommand = require('./commands/standings');
7+
const predictCommand = require('./commands/predict');
58

69
process.on('unhandledRejection', (reason) => {
710
console.log('Reason: ' + reason);
@@ -34,6 +37,24 @@ const yargs = require('yargs') // eslint-disable-line
3437
default: '.cached'
3538
})
3639
}, importCommand)
40+
.command('rank', 'import teams', (yargs) => {
41+
yargs.option('dbPath', {
42+
describe: 'database to read team data from and write elo rankings to',
43+
default: '.cached/database.sqlite'
44+
})
45+
}, rankCommand)
46+
.command('standings', 'get current standings', (yargs) => {
47+
yargs.option('dbPath', {
48+
describe: 'database to read team data from and write elo rankings to',
49+
default: '.cached/database.sqlite'
50+
})
51+
}, standingsCommand)
52+
.command('predict', 'predict a single match', (yargs) => {
53+
yargs.option('dbPath', {
54+
describe: 'database to read team data from and write elo rankings to',
55+
default: '.cached/database.sqlite'
56+
})
57+
}, predictCommand)
3758
.option('verbose', {
3859
alias: 'v',
3960
default: false

0 commit comments

Comments
 (0)