private SkillGaussian CalculateTeamRating(IEnumerable <JamPlayerEffectiveness> side, IEnumerable <SkillGaussian> players) { if (side.Count() > 5) { throw new InvalidOperationException("too many players on team!"); } // for each player, we need their mean and their playtime portion double summedQuality = 0; double summedVariance = 0; foreach (JamPlayerEffectiveness player in side) { // we scale the jammer to be half of the total importance of the team // we're adding in the beta variance here, rather than on the player, // to make future updates to the player cleaner SkillGaussian playerSkill = players.First(p => p.ID == player.PlayerID); if (player.IsJammer) { summedQuality += player.JamPortion * playerSkill.Mean * 4; summedVariance += player.JamPortion * (playerSkill.Variance + _betaSquared) * 4; } else { summedQuality += player.JamPortion * playerSkill.Mean; summedVariance += player.JamPortion * (playerSkill.Variance + _betaSquared); } } return(new SkillGaussian(0, summedQuality, summedVariance)); }
private void AnalyzeJam(IEnumerable <JamPlayerEffectiveness> players, DateTime dateOfJam) { var teams = players.GroupBy(p => p.TeamID); var team1 = teams.First().ToList(); var team2 = teams.Last().ToList(); var skills1 = team1.Select(p => CreatePlayerSkill(p.PlayerID, p.IsJammer, dateOfJam)); var skills2 = team2.Select(p => CreatePlayerSkill(p.PlayerID, p.IsJammer, dateOfJam)); var team1Skill = CalculateTeamRating(team1, skills1); var team2Skill = CalculateTeamRating(team2, skills2); // the difference in team skills, adjusted by the beta, give us a sense of how we expected this matchup to go // that can be compared to the actual result double c = Math.Sqrt(team1Skill.Variance + team2Skill.Variance + 2 * _betaSquared); // in normal TrueSkill, a Beta lead in rating is interpreted as having about an 80% chance of a win. // in our implementation, it represents an 80% result; in the 0-penalty case, // this is approximately equal to getting about a 3.5 point delta on a jam // determine which team exceeded expectations // basically, what in trueskill would be the main value becomes our "draw" value, // and the actual result becomes our main value double meanDiff = team1Skill.Mean - team2Skill.Mean; double scaledMeanDiff = meanDiff / c; double trueSkillDenominator = SkillGaussian.CumulativeTo(scaledMeanDiff, 0, 1); double result = _pointDeltaCumulative[_jamTeamPointDeltaMap[players.First().JamID][team1.First().TeamID]]; // the denominator is effectively the percent result expected for team 1; // if the actual result is greater, team 1 overperformed double team1Multiplier = 1; if (trueSkillDenominator > result) { meanDiff = team2Skill.Mean - team1Skill.Mean; scaledMeanDiff = meanDiff / c; trueSkillDenominator = SkillGaussian.CumulativeTo(scaledMeanDiff, 0, 1); result = _pointDeltaCumulative[_jamTeamPointDeltaMap[players.First().JamID][team2.First().TeamID]]; team1Multiplier = -1; } double inverseResult = SkillGaussian.InverseCumulativeTo(result, 0, 1); double newDenominator = SkillGaussian.CumulativeTo(scaledMeanDiff - inverseResult, 0, 1); double v = (SkillGaussian.At(scaledMeanDiff - inverseResult, 0, 1)) / newDenominator; double w = v * (v + scaledMeanDiff - inverseResult); foreach (SkillGaussian player in skills1) { CalculateNewPlayerRating(c, team1Multiplier, v, w, player); } foreach (SkillGaussian player in skills2) { CalculateNewPlayerRating(c, -team1Multiplier, v, w, player); } }
// at some point, we'll need to take the current sigma of the player and increase it, probably based on time since last data private double CalculateTimeEffect(SkillGaussian player, DateTime newTime) { // how much do we want to scale the player's std dev? // it probably ought to be flat, not proportional // one way to think of it: how long would a well-understood player have to disappear to be treated as new again? // let's say two years-ish, and see what happens TimeSpan timeSpan = newTime - player.LastUpdated; if (timeSpan > _baseTimeSpan) { timeSpan = _baseTimeSpan; } double ratio = ((double)timeSpan.Ticks) / _baseTimeSpan.Ticks; double range = _baseSigma - player.Sigma; return(range * ratio); }
private SkillGaussian CreatePlayerSkill(int playerID, bool isJammer, DateTime dateOfGame) { // variances, not std dev, are sum-able // a^2 + b^2 != (a+b)^2 var dictionary = isJammer ? _jammerSkillDictionary : _blockerSkillDictionary; if (!dictionary.ContainsKey(playerID)) { SkillGaussian newPlayer = new SkillGaussian(playerID, 500, _baseVariance, isJammer, dateOfGame); dictionary[playerID] = newPlayer; return(newPlayer); } var startingPlayer = dictionary[playerID]; double tau = CalculateTimeEffect(startingPlayer, dateOfGame); startingPlayer.AddVariance(Math.Pow(tau, 2)); startingPlayer.LastUpdated = dateOfGame; return(startingPlayer); }
private void CalculateNewPlayerRating(double c, double team1Multiplier, double v, double w, SkillGaussian player) { double meanMultiplier = player.Variance / c; double stdDevMultiplier = meanMultiplier / c; double playerMeanDelta = (team1Multiplier * meanMultiplier * v); double newMean = player.Mean + playerMeanDelta; double newVariance = player.Variance * (1 - (w * stdDevMultiplier)); SkillGaussian newValues = new SkillGaussian(player.ID, newMean, newVariance, player.IsJammer, player.LastUpdated); if (player.IsJammer) { _jammerSkillDictionary[player.ID] = newValues; } else { _blockerSkillDictionary[player.ID] = newValues; } }