Skip to content

Instantly share code, notes, and snippets.

@danielrichman
Created July 5, 2015 13:40
Show Gist options
  • Save danielrichman/ce0ce259da276528a241 to your computer and use it in GitHub Desktop.
Save danielrichman/ce0ce259da276528a241 to your computer and use it in GitHub Desktop.
Standalone (w/o Postgres) tool to play with foosball scores. See https://github.com/joey9801/stat-tracker.
from __future__ import division, print_function
import json
import numpy as np
from scipy.stats import norm
class config:
def no_points_condition(reds, blues):
return "X" in reds or "X" in blues
# Handicaps for different team sizes
magic_K = [None, -0.6, 0, 0.2, -0.1] # 1-indexed
# This should be on the scale of typical differences between skills
sigma = 1
def _tau_phi(reds, blues):
"""Probability that the red team wins a certain point"""
assert set(reds.keys()) & set(blues.keys()) == set()
assert 1 <= len(reds) <= 4
assert 1 <= len(blues) <= 4
absI = float(len(reds))
absJ = float(len(blues))
K_I = magic_K[len(reds)]
K_J = magic_K[len(blues)]
tau = sum(reds.values()) / absI - sum(blues.values()) / absJ + K_I - K_J
phi = sigma * np.sqrt( 1 / absI + 1 / absJ )
return tau, phi
def _pwp_tauphi(tau, phi):
return norm.cdf( tau / phi )
def _point_win_probability(reds, blues):
return _pwp_tauphi(*_tau_phi(reds, blues))
def skill_update(reds, blues, red_score, blue_score):
"""
reds, blues should be dictionaries mapping player_id to current
skill.
Returns two floats: delta_red, delta_blue
"""
assert red_score >= 0 and blue_score >= 0
assert red_score + blue_score > 0
assert red_score != blue_score
if config.no_points_condition(reds, blues):
return 0., 0.
tau, phi = _tau_phi(reds, blues)
E = _pwp_tauphi(tau, phi)
observed_E = red_score / (red_score + blue_score)
# since E \in (0, 1), target_E \in (0, 1) and so the PPF
# will hopefully not be +/- inf :-)
target_E = 0.8 * E + 0.2 * observed_E
x = norm.ppf(target_E)
delta = (x * phi - tau) * 0.5
return delta, -delta
def predict_score(reds, blues, points=10):
"""
Predict the score of a game; returns red_score, blue_score
"""
E = _point_win_probability(reds, blues)
if E > 0.5:
return points, points * (1 - E) / E
else:
return points * E / (1 - E), points
def _get_scores(scores, players):
return {k: scores.get(k, 0.0) for k in players}
def update_score(game, scores):
"""Updates players score for a single game_id"""
red_score = game["scores"]["red"]
blue_score = game["scores"]["blue"]
reds = _get_scores(scores, game["teams"]["red"])
blues = _get_scores(scores, game["teams"]["blue"])
adj_red, adj_blue = skill_update(reds, blues, red_score, blue_score)
for old_scores, adj in [(reds, adj_red), (blues, adj_blue)]:
for k, v in old_scores.items():
scores[k] = v + adj
def lookup_predict_score(scores, reds, blues):
"""
Predicts the scores from a list of team members
Returns red_score, blue_score
"""
reds = _get_scores(scores, reds)
blues = _get_scores(scores, blues)
return predict_score(reds, blues)
def lookup_predict_updates(scores, reds, blues):
"""
Predicts the score updates for various potential final scores.
Returns a dictionary mapping possible final scores (a pair of integers)
to pairs (red_adjustment, blue_adjustment).
"""
reds = _get_scores(scores, reds)
blues = _get_scores(scores, blues)
scores = [(10, i) for i in range(10)] + [(i, 10) for i in range(10)]
return {(red_score, blue_score): skill_update(reds, blues, red_score, blue_score)
for red_score, blue_score in scores}
def calculate(games):
scores = {}
for g in games:
update_score(g, scores)
return scores
def print_scores(scores):
for k, v in sorted(scores.items(), key=lambda x: x[1]):
print("{:10} {:.0f}".format(k, (v + 1.0) * 1000.0))
if __name__ == "__main__":
with open("foosball.json") as f:
games = json.load(f)
print_scores(calculate(games))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment