<jupyter_start><jupyter_text>Core> Team-strength models in Python<jupyter_code>#hide
from nbdev.showdoc import *
import json
import datetime as dt
import numpy as np
import pprint
#export

# For now, just re-export everything
from mezzala.adapters import *
from mezzala.blocks import *
from mezzala.models import *
from mezzala.weights import *
from mezzala.parameters import *<jupyter_output><empty_output><jupyter_text>Let's demo<jupyter_code>with open('../data/premier-league-1516.json', 'r') as f:
    pl_1516 = json.load(f)

# Let's parse the dates, too
for match in pl_1516:
    match['date'] = dt.datetime.strptime(match['date'], '%Y-%m-%d')
    
pl_1516[0:3]<jupyter_output><empty_output><jupyter_text>A model in `mezzala` is composed of 2 parts:* Model blocks (see `mezzala.blocks`)* An adapter (see `mezzala.adapters`) The model blocks determine which terms your model estimates. In general, you will wantto estimate offensive and defensive strength for each team (`TeamStrength`) and as well as home advantage `HomeAdvantage`.The selected model blocks can be supplied to the model as a list:<jupyter_code>blocks = [TeamStrength(), HomeAdvantage()]<jupyter_output><empty_output><jupyter_text>An adapter connects your model to the data source. In other words, it tells the model how find the information needed to fit.The information needed is determined by which model blocks are used. In our case,* All models require `home_goals` and `away_goals`* `TeamStrength` - requires `home_team` and `away_team``HomeAdvantage` doesn't require any information, since it assumes all matches have equal home-field advantage by default.<jupyter_code>adapter = KeyAdapter(               # `KeyAdapter` = data['...']
    home_team='team1',
    away_team='team2',
    home_goals=['score', 'ft', 0],  # Get nested fields with lists of fields
    away_goals=['score', 'ft', 1],  # i.e. data['score']['ft'][1]
)<jupyter_output><empty_output><jupyter_text>Pulling this together, we can construct a model from an adapter and blocks<jupyter_code>model = DixonColes(adapter=adapter, blocks=blocks)
model.fit(pl_1516)

# All estimates should be valid numbers
assert all(not np.isnan(x) for x in model.params.values())

# Home advantage should be positive
assert 1.0 < np.exp(model.params[HFA_KEY]) < 2.0<jupyter_output><empty_output><jupyter_text>Let's inspect the parameters a bit. First, let's look at the boring (non-team) ones:<jupyter_code>param_keys = model.params.keys()
param_key_len = max(len(str(k)) for k in param_keys)

for k in param_keys:
    if not isinstance(k, TeamParameterKey):
        key_str = str(k).ljust(param_key_len + 1)
        print(f'{key_str}: {np.exp(model.params[k]):0.2f}')<jupyter_output>ParameterKey(label='Home-field advantage')           : 1.23
ParameterKey(label='Rho')                            : 0.94<jupyter_text>And the team ones. Let's look at each team's attacking quality:<jupyter_code>teams = {k.label for k in param_keys if isinstance(k, TeamParameterKey)}

team_offence = [(t, np.exp(model.params[OffenceParameterKey(t)])) for t in teams]
for team, estimate in sorted(team_offence, key=lambda x: -x[1]):
    print(f'{team}: {estimate:0.2f}')
team_defence = [(t, np.exp(model.params[DefenceParameterKey(t)])) for t in teams]
for team, estimate in sorted(team_defence, key=lambda x: x[1]):
    print(f'{team}: {estimate:0.2f}')<jupyter_output>Manchester United FC: 0.82
Tottenham Hotspur FC: 0.84
Leicester City FC: 0.86
Arsenal FC: 0.86
Southampton FC: 0.97
Manchester City FC: 0.99
West Bromwich Albion FC: 1.10
Watford FC: 1.17
Liverpool FC: 1.19
Crystal Palace FC: 1.19
Swansea City FC: 1.21
West Ham United FC: 1.22
Chelsea FC: 1.26
Stoke City FC: 1.28
Everton FC: 1.32
Sunderland AFC: 1.46
Newcastle United FC: 1.52
Norwich City FC: 1.55
AFC Bournemouth: 1.57
Aston Villa FC: 1.75<jupyter_text>Making predictions for a single match<jupyter_code>scorelines = model.predict_one({
    'team1': 'Manchester City FC',
    'team2': 'Swansea City FC',
})

# Probabilities should sum to 1
assert np.isclose(
    sum(p.probability for p in scorelines),
    1.0
)

scorelines[0:5]
outcomes = scorelines_to_outcomes(scorelines)

# MCFC should have a better chance of beating Swansea
# at home than Swansea do of winning away
assert outcomes[Outcomes('Home win')].probability > outcomes[Outcomes('Away win')].probability

list(outcomes.values())<jupyter_output><empty_output><jupyter_text>Or for multiple matches<jupyter_code>many_scorelines = model.predict([
    {'team1': 'Manchester City FC',
     'team2': 'Swansea City FC'},
    {'team1': 'Manchester City FC',
     'team2': 'West Ham United FC'}
])<jupyter_output><empty_output><jupyter_text>What about a model with a different weighting method?By default, the `DixonColes` model weights all matches equally. However, it's more realistic to give matchescloser to the current date a bigger weight than those a long time ago.The original Dixon-Coles paper suggests using an exponential weight, and we can use the same:<jupyter_code>season_end_date = max(match['date'] for match in pl_1516)

weight = ExponentialWeight(
    # Value of `epsilon` is taken from the original paper
    epsilon=-0.0065,  
    key=lambda x: (season_end_date - x['date']).days
)
model_exp = DixonColes(
    adapter=adapter,
    blocks=blocks,
    weight=weight
)
model_exp.fit(pl_1516)<jupyter_output><empty_output><jupyter_text>How much does that change the ratings at season-end?<jupyter_code>for k in sorted(param_keys, key=lambda x: x.label):
    key_str = str(k).ljust(param_key_len + 1)
    model_param = np.exp(model.params[k])
    model_exp_param = np.exp(model_exp.params[k])
    print(f'{key_str}: {model_param:0.2f} -> {model_exp_param:0.2f} ({model_exp_param/model_param:0.2f})')<jupyter_output>OffenceParameterKey(label='AFC Bournemouth')         : 0.89 -> 0.88 (0.99)
DefenceParameterKey(label='AFC Bournemouth')         : 1.57 -> 1.61 (1.02)
OffenceParameterKey(label='Arsenal FC')              : 1.25 -> 1.25 (1.00)
DefenceParameterKey(label='Arsenal FC')              : 0.86 -> 0.85 (0.98)
OffenceParameterKey(label='Aston Villa FC')          : 0.54 -> 0.49 (0.91)
DefenceParameterKey(label='Aston Villa FC')          : 1.75 -> 1.83 (1.04)
OffenceParameterKey(label='Chelsea FC')              : 1.15 -> 1.20 (1.04)
DefenceParameterKey(label='Chelsea FC')              : 1.26 -> 1.16 (0.92)
OffenceParameterKey(label='Crystal Palace FC')       : 0.76 -> 0.70 (0.92)
DefenceParameterKey(label='Crystal Palace FC')       : 1.19 -> 1.25 (1.05)
OffenceParameterKey(label='Everton FC')              : 1.16 -> 1.02 (0.88)
DefenceParameterKey(label='Everton FC')              : 1.32 -> 1.33 (1.01)
ParameterKey(label='Home-field advantage')           : 1.23 -> 1.30 (1.05)
OffenceParameterKey(label[...]
{'fraction_non_alphanumeric': 0.12496307237813885, 'fraction_numerical': 0.03766617429837518, 'mean_word_length': 4.5318627450980395, 'pattern_counts': {'":': 0, '<': 44, '<?xml version=': 0, '>': 58, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '535457', 'license': 'Apache-2.0', 'n_tokens_mistral': 2484, 'n_tokens_neox': 2307, 'n_words': 649, 'path': 'nbs/core.ipynb', 'repo_name': 'Torvaney/mezzala', 'size': '6770'}