Created
July 5, 2015 13:40
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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