Skip to content

Commit

Permalink
Merge pull request #2 from syarhei/tests/MathModels
Browse files Browse the repository at this point in the history
add tests for math models
  • Loading branch information
syarhei authored May 5, 2018
2 parents c1018cc + c55120d commit d082693
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 90 deletions.
4 changes: 3 additions & 1 deletion app/inversify/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
DATABASE_CONTEXT, KEY, MAHER_POISSON_SERVICE, MAIN_CONTROLLER, MATCH_CATEGORY_DAL, MATCH_CATEGORY_MODEL,
MATCH_CONTROLLER, MATCH_DAL, MATCH_MODEL, MATCH_SERVICE, PASSPORT, SESSION_CONTROLLER, TEAM_CONTROLLER, TEAM_DAL,
TEAM_MODEL, TEAM_SERVICE, USER_CONTROLLER, USER_DAL, AUTH_HANDLER, USER_MODEL, USER_SERVICE, MATCH_CATEGORY_SERVICE,
MATCH_CATEGORY_CONTROLLER, DATABASE_SESSION
MATCH_CATEGORY_CONTROLLER, DATABASE_SESSION, BRADLEY_TERRY_SERVICE
} from "./identifiers/common";
import {IConfig} from "../types/IConfig";
import {IKey} from "../types/IKey";
Expand Down Expand Up @@ -39,6 +39,7 @@ import {UserService} from "../src/services/UserService";
import {MatchCategoryService} from "../src/services/MatchCategoryService";
import {MatchCategoryController} from "../src/routes/MatchCategoryController";
import {DBSession} from "../src/DB/DBSession";
import {BradleyTerryService} from "../src/services/BradleyTerryService";

const environment: string = getConfigEnvironment();
const config: IConfig = require(`../../configs/${environment}/config.json`);
Expand Down Expand Up @@ -68,6 +69,7 @@ container.bind<TeamService>(TEAM_SERVICE).to(TeamService);
container.bind<MatchCategoryService>(MATCH_CATEGORY_SERVICE).to(MatchCategoryService);
container.bind<MatchService>(MATCH_SERVICE).to(MatchService);
container.bind<MaherPoissonService>(MAHER_POISSON_SERVICE).to(MaherPoissonService);
container.bind<BradleyTerryService>(BRADLEY_TERRY_SERVICE).to(BradleyTerryService);
container.bind<CoefficientService>(COEFFICIENT_SERVICE).to(CoefficientService);

container.bind<TeamController>(TEAM_CONTROLLER).to(TeamController);
Expand Down
1 change: 1 addition & 0 deletions app/inversify/identifiers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const TEAM_SERVICE: symbol = Symbol("TEAM_SERVICE");
export const MATCH_CATEGORY_SERVICE: symbol = Symbol("MATCH_CATEGORY_SERVICE");
export const MATCH_SERVICE: symbol = Symbol("MATCH_SERVICE");
export const MAHER_POISSON_SERVICE: symbol = Symbol("MAHER_POISSON_SERVICE");
export const BRADLEY_TERRY_SERVICE: symbol = Symbol("BRADLEY_TERRY_SERVICE");
export const COEFFICIENT_SERVICE: symbol = Symbol("COEFFICIENT_SERVICE");

export const TEAM_CONTROLLER: symbol = Symbol("TEAM_CONTROLLER");
Expand Down
116 changes: 69 additions & 47 deletions app/scripts/TeamStatisticCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import uuid = require("uuid");
import {MatchCategory} from "../src/contracts/MatchCategory";
import container from "../inversify/config";
import {DATABASE_CONTEXT} from "../inversify/identifiers/common";
import {DBContext} from "../src/DB/DBContext";
import {Match} from "../src/contracts/Match";
import {Op} from "sequelize";

interface TeamValue {
key: string;
Expand All @@ -28,27 +28,57 @@ interface MatchValue {
const { clubs }: { name: string, clubs: TeamValue[] } = require("../../statistics/2012-13/en.1.clubs.json");
const BPL: { name: string, rounds: Round[] } = require("../../statistics/2012-13/en.1.json");

let categoryId: string = uuid();
let teams: Team[] = null;
const COUNTRY: string = "England";
let categoryId: string = uuid();

const teams: Team[] = clubs.map(club => {
return {
id: uuid(),
name: club.name,
year: 2018,
country: COUNTRY,
owner: "Siarhei Murkou"
} as Team;
});

const matchValues: MatchValue[] = [];
BPL.rounds.forEach(round => {
matchValues.push(...round.matches);
BPL.rounds.forEach((round, index, rounds) => {
if (index !== rounds.length - 1) {
matchValues.push(...round.matches);
}
});
const matches: Match[] = matchValues.map(match => {
return {
id: uuid(),
matchCategoryId: categoryId,
teamHomeId: teams.find(team => team.name === match.team1.name).id,
teamGuestId: teams.find(team => team.name === match.team2.name).id,
date: Date.parse(match.date),
place: COUNTRY,
homeGoals: match.score1,
guestGoals: match.score2
} as Match;
});

const context = container.get<DBContext>(DATABASE_CONTEXT);
export function getMatchesOfLastRound(): Match[] {
return BPL.rounds[BPL.rounds.length - 1].matches.map(match => {
return {
id: uuid(),
matchCategoryId: categoryId,
teamHomeId: teams.find(team => team.name === match.team1.name).id,
teamGuestId: teams.find(team => team.name === match.team2.name).id,
date: Date.parse(match.date),
place: COUNTRY,
homeGoals: match.score1,
guestGoals: match.score2
} as Match;
})
}

const context: any = container.get(DATABASE_CONTEXT);

async function createTeams() {
try {
teams = clubs.map(club => {
return {
id: uuid(),
name: club.name,
year: 2018,
country: COUNTRY,
owner: "Siarhei Murkou"
} as Team;
});
await context.TEAM.bulkCreate(teams);
} catch (err) {
console.log(`Error during Team creation`);
Expand All @@ -74,44 +104,36 @@ async function createMatchCategory() {

async function createMatches() {
try {
const matches: Match[] = matchValues.map(match => {
return {
id: uuid(),
matchCategoryId: categoryId,
teamHomeId: teams.find(team => team.name === match.team1.name).id,
teamGuestId: teams.find(team => team.name === match.team2.name).id,
date: Date.parse(match.date),
place: COUNTRY,
homeGoals: match.score1,
guestGoals: match.score2
} as Match;
});
await context.MATCH.bulkCreate(matches);
} catch (err) {
console.log(`Error during Match creation`);
throw err;
}
}

export async function createFootballSet(): Promise<{ teams: Team[], categoryId: string }> {
try {
await createTeams();
await createMatchCategory();
await createMatches();
console.log("Football Statistic is uploaded to database");
return { teams, categoryId };
} catch (err) {
console.log(err);
}
export async function deleteFootballSet(matchIds: string[], categoryId: string, teamIds: string[]) {
await context.MATCH.destroy({ where: { id: { [Op.or]: matchIds } } });
await context.MATCH_CATEGORY.destroy({ where: { id: categoryId } });
await context.TEAM.destroy({ where: { id: { [Op.or]: teamIds } } });
console.log("Football Statistic is deleted from database");
}

(async () => {
try {
if (require.main === module) {
await createFootballSet();
process.exit(0);
}
} catch (err) {
process.exit(-1);
}
})();
export async function createFootballSet(): Promise<{ teams: Team[], categoryId: string, matches: Match[] }> {
await createTeams();
await createMatchCategory();
await createMatches();
console.log("Football Statistic is uploaded to database");
return { teams, categoryId, matches };
}

// (async () => {
// try {
// if (require.main === module) {
// await createFootballSet();
// process.exit(0);
// }
// } catch (err) {
// console.log(err);
// process.exit(-1);
// }
// })();
4 changes: 2 additions & 2 deletions app/src/models/MatchModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ export class MatchModel {
type: STRING(36),
primaryKey: true
},
"teamHome": {
"teamHomeId": {
type: STRING(36),
references: {
model: this.teamModel.model,
key: "id"
}
},
"teamGuest": {
"teamGuestId": {
type: STRING(36),
references: {
model: this.teamModel.model,
Expand Down
48 changes: 41 additions & 7 deletions app/src/routes/MatchController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import {Router, Request, Response} from "express";
import {Match} from "../contracts/Match";
import {CoefficientService} from "../services/CoefficientService";
import {MaherPoissonService} from "../services/MaherPoissonService";
import {COEFFICIENT_SERVICE, MAHER_POISSON_SERVICE, MATCH_SERVICE} from "../../inversify/identifiers/common";
import {
BRADLEY_TERRY_SERVICE, COEFFICIENT_SERVICE, MAHER_POISSON_SERVICE,
MATCH_SERVICE
} from "../../inversify/identifiers/common";
import * as wrap from "express-async-wrap";
import * as autoBind from "auto-bind";
import {BradleyTerryService} from "../services/BradleyTerryService";

const MATCH_LIMIT_NUMBER: number = 40;

Expand All @@ -14,25 +20,30 @@ export class MatchController {
@inject(MATCH_SERVICE) private matchService: MatchService,
@inject(COEFFICIENT_SERVICE) private coefficientService: CoefficientService,
@inject(MAHER_POISSON_SERVICE) private maherPoissonService: MaherPoissonService,
) {}
@inject(BRADLEY_TERRY_SERVICE) private bradleyTerryService: BradleyTerryService
) {
autoBind(this);
}

set router(router: Router) {}

get router(): Router {
const router: Router = Router();

router.post("/match/engine/maher-poisson", this.createMatchByMaherPoisson);
router.post("/match/engine/bradley-terry", this.createMatchByBradleyTerry);
router.post("/matches/engine/maher-poisson", wrap(this.createMatchByMaherPoisson));
router.post("/matches/engine/bradley-terry", wrap(this.createMatchByBradleyTerry));
// router.post("/match/expert-review", this.createMatch);

return router;
}

private async createMatchByMaherPoisson(req: Request, res: Response): Promise<void> {
const { teamHomeId, teamGuestId, matchCategoryId, date, place }: Match = req.body;
const { teamHomeId, teamGuestId, matchCategoryId, date, place }: Match = req.body as Match;

const { attack: homeAttack, defence: homeDefence } =
await this.matchService.getMatchesByTeam(teamHomeId, MATCH_LIMIT_NUMBER);
await this.matchService.getTeamAbilities(teamHomeId, MATCH_LIMIT_NUMBER);
const { attack: awayAttack, defence: awayDefence } =
await this.matchService.getMatchesByTeam(teamGuestId, MATCH_LIMIT_NUMBER);
await this.matchService.getTeamAbilities(teamGuestId, MATCH_LIMIT_NUMBER);

let { win1, draw, win2 } = this.maherPoissonService
.generateProbability(homeAttack, awayDefence, awayAttack, homeDefence);
Expand All @@ -48,6 +59,29 @@ export class MatchController {
}

private async createMatchByBradleyTerry(req: Request, res: Response): Promise<void> {
const { teamHomeId, teamGuestId, matchCategoryId, date, place }: Match = req.body as Match;

const [matchesWithHomeTeam, matchesWithGuestTeam] = await Promise.all<Match[], Match[]>([
this.matchService.getMatchesByTeamId(teamHomeId, MATCH_LIMIT_NUMBER),
this.matchService.getMatchesByTeamId(teamGuestId, MATCH_LIMIT_NUMBER)
]);

const point1: number = this.matchService.getTeamPoints(teamHomeId, matchesWithHomeTeam);
const point2: number = this.matchService.getTeamPoints(teamGuestId, matchesWithGuestTeam);
const drawProb1: number = this.matchService.getDrawProbability(matchesWithHomeTeam);
const drawProb2: number = this.matchService.getDrawProbability(matchesWithGuestTeam);
const homeAdvantage: number = this.matchService.getHomeAbility(teamHomeId, matchesWithHomeTeam);

let { win1, draw, win2 } = this.bradleyTerryService
.generateProbabilities(point1, point2);
({ win1, win2, draw } = this.coefficientService.getCoefficients(win1, draw, win2));

const data: Match = {
teamHomeId, teamGuestId, matchCategoryId, date, place,
coefficientWin1: win1, coefficientDraw: draw, coefficientWin2: win2
} as Match;
const match = await this.matchService.createMatchWithCoefficients(data);

res.status(201).json(match);
}
}
31 changes: 11 additions & 20 deletions app/src/services/BradleyTerryService.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import {injectable} from "inversify";

enum MatchResult {
WIN = 1, DRAW = 0.5, LOSE = 0
}
import {convertSumToInteger} from "../../utils/math";

@injectable()
export class BradleyTerryService {
private readonly B: number = Math.log(2) / 100;
constructor() {}

public getRating(oldRating: number, K: number, result: MatchResult, difference: number): number {
const power: number = ( difference / 400 ) + 1;
const expectedResult: number = 1 / ( Math.pow(10, power));
return oldRating + K * ( result - expectedResult );
}

public getStartingRating(oldRating: number, wins: number, loses: number, games: number): number {
return oldRating + ( 400 * (wins - loses) / games );
}

public generateProbability() {
public generateProbabilities(
point1: number, point2: number, drawProb1: number = 1, drawProb2: number = 1, homeAdvantage: number = 0.5
) {
homeAdvantage = homeAdvantage * 2;
let win1: number = homeAdvantage * point1 / (homeAdvantage * point1 + point2);
const numeratorOfDraw: number = 2 * Math.sqrt(drawProb1 * drawProb2 * homeAdvantage * point1 * point2);
let draw: number = numeratorOfDraw / (homeAdvantage * point1 + point2 + numeratorOfDraw);
let win2: number = point2 / (homeAdvantage * point1 + point2);

}
([win1, draw, win2] = convertSumToInteger(1, win1, draw, win2));

private getProbability(homeAdvantage: number, ratingHome: number, ratingAway: number): number {
const exp: number = Math.exp(homeAdvantage + this.B * (ratingHome - ratingAway));
return exp / (1 + exp);
return { win1, draw, win2 };
}
}
10 changes: 5 additions & 5 deletions app/src/services/MaherPoissonService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ type MaherPoissonEntity = { homeGoals: number; guestGoals: number; probability:
export class MaherPoissonService {
private readonly place: number = null;
private readonly TABLE_SIZE: number = null;
private maherPoissonTable: MaherPoissonEntity[] = [];

constructor(@inject(CONFIG) private config: IConfig) {
this.place = config.MAHER_POISSON_HOME_PLACE_COEFFICIENT;
Expand All @@ -27,23 +26,24 @@ export class MaherPoissonService {
public generateProbability(
homeAttack: number, awayDefence: number, awayAttack: number, homeDefence: number
): { win1: number, draw: number, win2: number } {
const maherPoissonTable: MaherPoissonEntity[] = [];
const home: number = Math.log(this.place + homeAttack + awayDefence);
const away: number = Math.log(awayAttack + homeDefence);

for (let homeGoals: number = 0; homeGoals < this.TABLE_SIZE; homeGoals++) {
for (let guestGoals: number = 0; guestGoals < this.TABLE_SIZE; guestGoals++) {
const probability: number = this.calculateProbability(home, homeGoals, away, guestGoals);
this.maherPoissonTable.push({ homeGoals, guestGoals, probability });
maherPoissonTable.push({ homeGoals, guestGoals, probability });
}
}

let win1: number = this.maherPoissonTable
let win1: number = maherPoissonTable
.filter(({homeGoals, guestGoals}: MaherPoissonEntity) => homeGoals > guestGoals)
.reduce<number>((memory: number, value) => memory + value.probability, 0);
let draw: number = this.maherPoissonTable
let draw: number = maherPoissonTable
.filter(({homeGoals, guestGoals}: MaherPoissonEntity) => homeGoals === guestGoals)
.reduce<number>((memory: number, value) => memory + value.probability, 0);
let win2: number = this.maherPoissonTable
let win2: number = maherPoissonTable
.filter(({homeGoals, guestGoals}: MaherPoissonEntity) => homeGoals < guestGoals)
.reduce<number>((memory: number, value) => memory + value.probability, 0);

Expand Down
Loading

0 comments on commit d082693

Please sign in to comment.