Spielen Sie mit statistischer Modellierung: Quantifizieren Sie die Stärke von J-League-Teams mit Stan und Python

Einführung

Neulich las ich das Buch Bayes Statistical Modeling mit Stan und R, das mich dazu brachte, statistische Modellierung auszuprobieren. Ich empfehle es, weil es ein sehr gutes Buch ist.

Stan kann nicht nur von R aus verwendet werden, sondern auch von der Python-Bibliothek PyStan. Dieses Mal werde ich ein Modell erstellen, das die Stärke (den gleichen Wert) jeder Mannschaft in der Soccer J1 League 2016 anhand der Spielergebnisse schätzt.

Diesmal ist die Hauptversion

Daten sammeln

Sie können die Ergebnisse der J-League-Spiele unter http://www.jleague.jp/stats/SFTD01.html überprüfen. Angenommen, Sie haben die folgenden Daten von jedem Team in der ersten Phase des Jahres 2016 in irgendeiner Weise gesammelt. https://gist.github.com/mokemokechicken/a26eb7230e51fba7018476f7706e5b0b

away_score away_team date home_score home_team score visitors
1 Nagoya 02/27(Boden) 0 Iwata 0-1 14333
1 Kawasaki F. 02/27(Boden) 0 Hiroshima 0-1 18120
1 Fukuoka 02/27(Boden) 2 Torisu 2-1 19762
... ... ... ... ... ... ...

Denken

Denken Sie über den Mechanismus nach, mit dem Übereinstimmungsergebnisse erzeugt werden

Nehmen wir an, wie das Match-Ergebnis (Score) wie folgt generiert wird.

Wird besorgt. Nun, es scheint bisher nicht so seltsam zu sein.

Als nächstes betreten wir die Welt der Statistik. Zunächst wird angemessen angenommen, dass die "Punktzahl" der Poisson-Verteilung folgt. Mit anderen Worten

Punktzahl eines Spiels eines Teams ~ Poisson (Angriffskraft dieses Teams - Verteidigungskraft des gegnerischen Teams)

Es ist wie es ist. (Übrigens bedeutet "~" "der Verteilung folgen".) Die Parameter der Poisson-Verteilung können jedoch keine negativen Werte annehmen. Wenn Sie also der Einfachheit halber die Ausgangsleistung zu "exp" hinzufügen,

Ein Spiel für eine Heimmannschaft erzielen~ Poisson(exp( 
		(Heimstrom+Angriffskraft der Heimmannschaft) -Auswärtsmannschaft Verteidigung
	))

Nehme an, dass Auch symmetrisch

Ein Spiel in einer Heimmannschaft kassiert~ Poisson(exp( 
Angriffskraft des Auswärtsteams- (Heimstrom+Die Verteidigung der Heimmannschaft)
	))

Wird besorgt.

Schreiben Sie das Modell in Stan

Es stellte sich heraus, dass es so etwas war.

model_code = """
data {
    int N;  // N Games
    int K;  // K Teams
    int Th[N]; // Home Team ID
    int Ta[N]; // Away Team ID
    int Sh[N]; // Home Team score point
    int Sa[N]; // Away Team score point
}

parameters {
    real atk[K];
    real def[K];
    real home_power[K];
    real<lower=0> sigma;
    real<lower=0> hp_sigma;
}

model {
    for (k in 1:K) {
        atk[k] ~ normal(0, sigma);
        def[k] ~ normal(0, sigma);
        home_power[k] ~ normal(0, hp_sigma);
    }

    for (n in 1:N) {
        Sh[n] ~ poisson(exp(
            (home_power[Th[n]] + atk[Th[n]]) - 
            (def[Ta[n]])
        ));

        Sa[n] ~ poisson(exp(
            (atk[Ta[n]]) - 
            (def[Th[n]] + home_power[Th[n]])
        ));
    }
}

generated quantities {
    real games[K, K, 2];
    
    for (th in 1:K) {
        for (ta in 1:K) {
            games[th, ta, 1] = poisson_rng(exp((home_power[th] + atk[th]) - (def[ta])));
            games[th, ta, 2] = poisson_rng(exp((atk[ta]) - (def[th] + home_power[th])));
        }
    }
}
"""

einfache Erklärung

Für Stan ist das Buch, das ich zuvor vorgestellt habe, sehr hilfreich, aber ich werde es auch hier kurz erklären.

Datenblock

data {
    int N;  // N Games
    int K;  // K Teams
    int Th[N]; // Home Team ID
    int Ta[N]; // Away Team ID
    int Sh[N]; // Home Team score point
    int Sa[N]; // Away Team score point
}

Dieser "Daten" -Block deklariert extern gegebene Daten. Die früher gesammelten Daten werden später mit diesem Namen versehen.

Parameterblock

parameters {
    real atk[K];
    real def[K];
    real home_power[K];
    real<lower=0> sigma;
    real<lower=0> hp_sigma;
}

Dieser Parameterblock beschreibt die Parameter, die Sie schätzen möchten. Dieses Mal liegt der Schwerpunkt auf Angriffskraft (atk), Verteidigungskraft (def) und Heimkraft (home_power). <lower = 0> repräsentiert den Bereich, den der Parameter einnehmen kann.

Modellblock

model {
    for (k in 1:K) {
        atk[k] ~ normal(0, sigma);
        def[k] ~ normal(0, sigma);
        home_power[k] ~ normal(0, hp_sigma);
    }

    for (n in 1:N) {
        Sh[n] ~ poisson(exp(
            (home_power[Th[n]] + atk[Th[n]]) - 
            (def[Ta[n]])
        ));

        Sa[n] ~ poisson(exp(
            (atk[Ta[n]]) - 
            (def[Th[n]] + home_power[Th[n]])
        ));
    }
}

Dieser "Modell" -Block drückt die Beziehung zwischen den Eingabedaten und den Parametern aus, die Sie mithilfe einer Wahrscheinlichkeitsverteilung schätzen möchten. Der zuvor erwähnte "Mechanismus" wird auch hier ausgedrückt. Außerdem wird angenommen, dass die Offensiv- und Defensivkraft jedes Teams der "Normalverteilung des Verteilungssigmas" mit einem Durchschnitt von 0 "folgt. Dieses "Sigma" ist auch (im Übrigen) ein geschätzter Parameter. Andernfalls würde die Berechnung (MCMC) nicht konvergieren (da alle Werte in verschiedenen Bereichswerten konvergieren würden). Nun, es scheint, als hätte ich eine Hoffnung geschrieben, dass "ich möchte, dass Sie vorerst in den Wert hier passen". Interessant ist, dass es für die Berechnung einfacher war, zu konvergieren (das konvergierte "Sigma" war viel kleiner als 1), als das "Sigma" als feste "1" zu schreiben. Durch die Hardcodierung 1 habe ich das Gefühl, dass der Bereich begrenzt ist, aber es ist interessant, dass dies nicht der Fall ist.

generierte Mengen blockieren

generated quantities {
    real games[K, K, 2];
    
    for (th in 1:K) {
        for (ta in 1:K) {
            games[th, ta, 1] = poisson_rng(exp((home_power[th] + atk[th]) - (def[ta])));
            games[th, ta, 2] = poisson_rng(exp((atk[ta]) - (def[th] + home_power[th])));
        }
    }
}

Dieser Block "generierte Mengen" ist wie ein Bonus. Diese Funktion berechnet anhand des konvergierten Werts anders. Hier werden die Parameter jeder Mannschaft verwendet, um die Punktzahl des Spiels abzutasten, wenn jede Mannschaft gegeneinander spielt. Sie können es auf der Python-Seite separat berechnen. Da die Berechnungslogik jedoch mit dem Modell übereinstimmen muss, scheint es einfacher zu sein, es zu verwalten, wenn Sie es hier verwalten.

Andere

Es gibt einen weiteren Block "transformierte Parameter", und Sie können die Variablen "Daten" und "Parameter" verwenden, um verschiedene Variablen zu definieren. Ausdrücke wie "(home_power [th] + atk [th]) - (def [ta])" erscheinen dieses Mal auch zweimal in "model" und "generierten Mengen", daher ist es besser, sie zusammenzusetzen. Ich frage mich, aber ...

Berechnen Sie mit Modell und Daten

Definieren Sie einige nützliche Funktionen

from hashlib import sha256
import pickle
import time

import requests
import pandas as pd
from pystan import StanModel


def stan_cache(file=None, model_code=None, model_name=None, cache_dir="."):
    """Use just as you would `stan`"""
    # http://pystan.readthedocs.io/en/latest/avoiding_recompilation.html#automatically-reusing-models

    if file:
        if file.startswith("http"):
            model_code = requests.get(file).text
        else:
            with open(file) as f:
                model_code = f.read()
    code_hash = sha256(model_code.encode('utf8')).hexdigest()
    if model_name is None:
        cache_fn = '{}/cached-model-{}.pkl'.format(cache_dir, code_hash)
    else:
        cache_fn = '{}/cached-{}-{}.pkl'.format(cache_dir, model_name, code_hash)
    try:
        sm = pickle.load(open(cache_fn, 'rb'))
    except:
        start_time = time.time()
        sm = StanModel(model_code=model_code)
        print("compile time: %s sec" % (time.time() - start_time))
        with open(cache_fn, 'wb') as f:
            pickle.dump(sm, f)
    return sm


def sampling_summary_to_df(fit4model):
    """

    :param StanFit4model fit4model:
    :return:
    """
    s = fit4model.summary()
    return pd.DataFrame(s["summary"], columns=s['summary_colnames'], index=s['summary_rownames'])

Die Funktion "stan_cache" speichert das Stan-Modell zwischen und ruft den Modellcode auch über HTTP ab. Praktisch.

Die Funktion sample_summary_to_df wandelt das Ergebnis der Anpassung des Modells in einen Pandas DataFrame um. Es hat den Vorteil, dass es bei der Arbeit mit Jupyter leichter zu erkennen ist.

Lauf

import numpy as np
import pandas as pd

df = pd.read_csv("https://gist.githubusercontent.com/mokemokechicken/a26eb7230e51fba7018476f7706e5b0b/raw/90e1c26e172b92d5f52d53d0591a611e0258bd2b/2016-j1league-1st.csv")

team_list = list(sorted(set(df['away_team']) | set(df['home_team'])))
teams = dict(zip(range(1, len(team_list)+1), team_list))
for k, v in list(teams.items()):
    teams[v] = k
    
model = stan_cache(model_code=model_code) 

stan_input = {
    "N": len(df),
    "K": len(team_list),
    "Th": df['home_team'].apply(lambda x: teams[x]),
    "Ta": df['away_team'].apply(lambda x: teams[x]),
    "Sh": df['home_score'],
    "Sa": df['away_score'],
}

fitted = model.sampling(data=stan_input, seed=999)
sampling_summary_to_df(fitted).sort_values("Rhat", ascending=False)

Ergebnis

Eine Zusammenfassung der Parameterschätzungsergebnisse lautet beispielsweise wie folgt. In dem Buch "Wenn dieses" Rhat "für alle geschätzten Parameter kleiner als 1,1 ist, kann davon ausgegangen werden, dass die Berechnung konvergiert hat", daher werde ich davon ausgehen, dass es vorerst konvergiert. Es ist jedoch ein wenig subtil, dass die "Häufigkeit, mit der der Wert richtig abgetastet werden kann" mit der Bezeichnung "n_eff" weniger als 100 beträgt (insbesondere beim Schätzen des Intervalls), daher gibt es einige Feinheiten, aber dieses Mal werde ich es als gut belassen.

mean se_mean sd 2.5% 25% 50% 75% 97.5% n_eff Rhat
lp__ -214.114891 1.542823 13.712915 -237.604900 -223.960556 -215.747425 -205.033248 -183.817981 79.0 1.049519
hp_sigma 0.094069 0.005835 0.063921 0.011408 0.044970 0.081836 0.132181 0.245524 120.0 1.031258
atk[12] 0.048430 0.009507 0.158794 -0.318161 -0.052664 0.052359 0.155878 0.342186 279.0 1.018430
atk[16] 0.225840 0.008570 0.158951 -0.075769 0.115129 0.223274 0.330571 0.531899 344.0 1.013612
sigma 0.220279 0.002456 0.056227 0.112035 0.182132 0.217550 0.255500 0.344382 524.0 1.011379
... ... ... ... ... ... ... ... ... ... ...

Zeigen Sie die Ergebnisse an

Geschätzte Teamstärke

Sammeln Sie die Parameter nach Team.

tk = {}

params = fitted.extract()
atks = params['atk']
defs = params['def']
home_power = params['home_power']
games = params['games']


for k, team in enumerate(team_list):
    tk[team] = {
            "atk": np.mean(atks[:, k]),
            "def": np.mean(defs[:, k]),
            "home_power": np.mean(home_power[:, k]),
    }

tdf = pd.DataFrame(tk).T

Vorerst werde ich sie in absteigender Reihenfolge der "Heimatmacht" anordnen.

tdf.sort_values("home_power", ascending=False)
atk def home_power
Kashima 0.225840 0.217699 0.068898
Urawa 0.152638 0.063192 0.041830
Kobe 0.099154 -0.163383 0.030443
Hiroshima 0.293808 -0.000985 0.029861
Nagoya 0.130757 -0.247969 0.015849
Kawasaki F. 0.312593 0.087859 0.014728
Niigata 0.010827 -0.146252 0.011885
Iwata 0.048430 -0.105230 0.011843
Sendai 0.037960 -0.150151 0.005462
FC Tokio -0.072115 0.017485 -0.005050
G Osaka 0.087034 -0.033666 -0.005532
Torisu -0.232595 0.103412 -0.006186
Eiche 0.036172 -0.053332 -0.009568
Omiya -0.040014 0.027842 -0.020942
Yokohama FM 0.059420 -0.009322 -0.021393
Fukuoka -0.185292 -0.130759 -0.026643
Kofu 0.002608 -0.268061 -0.037079
Shonan -0.000052 -0.184515 -0.049440

ist geworden. Dieses Mal sollte die durchschnittliche Stärke 0 sein. Ob sie also höher oder niedriger als 0 ist, ist mehr als der Durchschnitt des J1-Teams.

Übrigens, wenn Sie es mit den Daten der 2. Stufe tun, erhalten Sie ein völlig anderes Ergebnis (Kashima wird viel schlechter sein). Bis zum letzten ist es genau so, wenn es aus den Daten der 1. Stufe berechnet wird, also bin ich mir nicht sicher.

Wenn Sie darüber nachdenken, ist es natürlich, aber es liegt nahe an der 1. Etappe 2016. https://ja.wikipedia.org/wiki/2016%E5%B9%B4%E3%81%AEJ1%E3%83%AA%E3%83%BC%E3%82%B0#.E9.A0.86.E4.BD.8D.E8.A1.A8 Insbesondere scheint es eine allgemeine Korrelation zwischen "Gesamtpunkten und Gesamtzielen" und "Angriffs- und Verteidigungskraft" zu geben (obwohl es ein Problem wäre, wenn es keine gäbe).

schließlich

Ich habe versucht zu simulieren, was passieren würde, wenn ich den Toto der 2. Stufe mit diesem Vorhersagemodell kaufen würde, aber ich habe aufgehört, weil es ein schreckliches Ergebnis zu sein scheint (lacht). Es scheint, dass wir gute Vorhersagen treffen können, wenn die Wettbewerbsdichte in kurzer Zeit hoch ist. Sagen Sie 17 Verse mit Daten bis zu 16 Versen voraus. Ist das etwas besser?

Es ist erstaunlich, dass diese Art des Modellierens einfach geworden ist und viel Spaß macht.

Recommended Posts

Spielen Sie mit statistischer Modellierung: Quantifizieren Sie die Stärke von J-League-Teams mit Stan und Python
Entenbuch in Python implementiert "Bayes statistische Modellierung mit Stan und R"
Fraktal zum Erstellen und Spielen mit Python
"Einführung in die Datenanalyse durch statistische Bayes'sche Modellierung beginnend mit R und Stan" in Python implementiert
Spielen Sie mit 2016-Python
Lesen von Notizen (in Python und Stan) zur Einführung in die statistische Modellierung für die Datenanalyse (Midorimoto)
[Python] Wie man mit Klassenvariablen mit Dekorator und Metaklasse spielt
[Lass uns mit Python spielen] Bildverarbeitung zu Monochrom und Punkten
Spielen Sie mit Mastodons Archiv in Python 2 Count Antworten und Favoriten
Spielen Sie mit dem Passwortmechanismus von GitHub Webhook und Python
Programmieren mit Python und Tkinter
Ver- und Entschlüsselung mit Python
Python und Hardware-Verwenden von RS232C mit Python-
[Python] Spielen Sie mit Discords Webhook.
Python mit Pyenv und Venv
Funktioniert mit Python und R.
Einführung in die Bayes'sche statistische Modellierung mit Python ~ Versuch einer linearen Regression mit MCMC ~
Kommunizieren Sie mit FX-5204PS mit Python und PyUSB
Leuchtendes Leben mit Python und OpenCV
Roboter läuft mit Arduino und Python
Installieren Sie Python 2.7.9 und Python 3.4.x mit pip.
Neuronales Netzwerk mit OpenCV 3 und Python 3
AM-Modulation und Demodulation mit Python
Scraping mit Node, Ruby und Python
Scraping mit Python, Selen und Chromedriver
Kratzen mit Python und schöner Suppe
Lass uns mit Python mit Python spielen [Anfänger]
JSON-Codierung und -Decodierung mit Python
Hadoop-Einführung und MapReduce mit Python
Lesen und Schreiben von NetCDF mit Python
Lesen und Schreiben von CSV mit Python
Mehrfachintegration mit Python und Sympy
Koexistenz von Python2 und 3 mit CircleCI (1.0)
Sugoroku-Spiel und Zusatzspiel mit Python
FM-Modulation und Demodulation mit Python