keskiviikko 11. toukokuuta 2016

MM-jääkiekon turnausformaatin mallinnusta

MM-kisojen pistelaskusysteemi antaa aina aivoilleni virikkeitä. Systeemi tuntuu omituiselta, mutta voin tietokoneohjelmalla osoittaa, että se toimii.

Pistelasku kausilla 2015 ja 2016

Turnaus alkaa alkusarjalla, joka kestää valtaosan turnauksesta. Joukkueet ovat valmiiksi jaettu kahteen erilliseen lohkoon A ja B. Lohkot on muodostettu aiempiin kisoihin perustuvan ranking-sijoituksen perusteella niin, että ne olisivat mahdollisimmat tasavahvoja.

Kummankin lohkon kaikki kahdeksan joukkuetta pelaavat toisiaan vastaan. Ottelut pelataan ratkaisuun asti. Jos ratkaisu tapahtuu varsinaisella peliajalla, pisteet jaetaan voittajan eduksi 3-0. Jatkoerässä tai rankkarikisassa (viralliselta nimeltä voittomaalikilpailussa) ratkaistussa ottelussa pisteet jaetaan tasaisemmin, 2-1.

Kumpikin lohko järjestestetään sen joukkueiden saamien pisteiden mukaisesti. Lohkon ykkönen on se, joka saa eniten pisteitä. Neljän vähiten pisteitä saaneen joukkueen pelit päättyvät. Niiden sijoitus turnauksessa selviää ensisijaisesti lohkosijoituksen mukaan. Jos ne ovat tasan, ratkaisee ensin pisteet, ja sen jälkeen muut tekijät.

Turnaus päättyy pudotuspeliin. Puolivälierässä lohkon A joukkueet pelaavan B-lohkon joukkueita vastaan käänteisessä järjestyksessä. A1 pelaa B4 vastaan, A2 pelaa B3 vastaan, jne. Välierissä ovat vastakkain 1A/4B – 2B/3A ja 1B/4A – 2A/3B. Voittajat jatkavat finaaliin ja hävinneet pronssiotteluun.

Tietokoneohjelmaa rakentamaan

Tietokoneohjelmaa tehdässä asioita kannattaa alussa yksinkertaistaa.

Joukkue on yhtäkuin sen vahvuustasoa kuvaava kokonaisluku. Perättäiset kokonaisluvut 0, 1, 2, ... luodaan komennolla.
joukkueet = range(16)
Alkulohkojen lohkojako määräytyy edellisten kisojen perusteella. Kokeilin kuitenkin muodostaa lohkot sattumanvarasesti sekoittaen listan, ja jakamalla se kahteen lohkoon.
random.shuffle(joukkueet)  
lohkoA = joukkueet[0:8]
lohkoB = joukkueet[8:18]
Sitten mallinnetaan alkusarja, jossa kaikki pelaa toisiaan vastaan. Koodin voisi rakentaa hätäisesti kahdesta silmukasta, mutta pythonissa on tätä varten ns. syntaktista sokeria. Tässäkin on yksinkertaistettu. Todellisuudessa pitäisi ottaa huomioon vielä otteluiden jakautuminen tasaisesti, sekä ajallisesti, että koti- ja vieraslogiikan suhteen.
for (id1, jo1), (id2, jo2) in itertools.combinations(enumerate(lohko), 2):

Voittaja saa ohjelmassani aina 3 pistettä. Jatkoaikaa tai rankkarikisaa ei ole mallinnettu.

Jatkopeleissä olen käyttänyt hyväksi järjestettyjä taulukoita: Sekä alkusarja-funktio että ottelufunktio palauttaa tulokset taulukkona, jonka solun järjestysnumero vastaa sijoitusta. Siten on helppoa osoittaa jatkopeliin valittua joukkuetta:

finaali = pelaa_ottelu_sort(valiera1[0], valiera2[0])
pronssiottelu = pelaa_ottelu_sort(valiera1[1], valiera2[1])
Lopuksi testataan vielä, onko tuloslista todenmukainen.

Ohjelman suorituksen perusteella neljä parasta joukkuetta menee aina täysin oikein, mikäli oletetaan, että parempi joukkue voittaa aina. Kisoissa pelataan 64 ottelua, ohjelmassani tehdään yhtä monta vertailua, ja tämä riittää neljän parhaan joukkueen löytämiseen. Koska millään joukkueella ei ole samaa taitotasoa, tasapelejä ei ilmene.
[15, 14, 13, 12]

Kiekko pomppii aina

Ongelmat alkavat, kun otetaan huomioon otteluiden tulosten sattumanvaraisuus. Turnausformaatin olisi siedettävä yksittäinen ottelukohtainen vaihtelu. Se ei saisi johtaa merkittävästi väärään lopputulokseen, ja joukkuetta tulisi arvioida pelattujen otteluiden keskiarvon mukaan.

Pienet vaihtelut joukkueiden taitotasossa ovat todennäköisempiä kuin suuret, eikä ole odotettua, että kaikki asiat menisivät joukkueessa pieleen. Oletan, että joukkueiden tasovaihtelu noudattaa normaalijakaumaa eli gaussinkäyrää. (Juhani Tamminen on kylläkin puhunut jääkiekon momenttum-ilmiöstä, mutta en tässä ota siihen kantaa.) Pythonissa on satunnaislukugeneraattori, joka laskee gaussin jakaumalla. Tulos ottelusta saadaan yksinkertaisesti laskemalla taitotason erotus, ja lisäämällä siihen satunnaisuutta gauss-funktiolla.
tulos = joukkue1 - joukkue2 + random.gauss(0, varianssi)
 Varianssi vaikuttaa pistelaskun onnistumiseen melkoisesti.


Jatkopohdiskelua


Tulokset olisivat vähän parempia, jos noteerattaisiin jatkoaika alkusarjan pistetuloksista. Näin ottelun tulokset saadaan tarkemmin, ja joukkueiden järjestys sarjan jälkeen määräytyy todenmukaisemmin. Kokeilin alustavasti, ja parhaimmillaan saavutetaan 5% tarkempia tuloksia.

Alkulohkojen järjestäminen aikaisempien tulosten perusteella auttaa, mutta pääasiassa vain sijoituksilla 5-16.

Vielä kuriositeettina QuickSort-funktion käyttö turnauksessa. Kuvassa virallinen pistejärjestelmä sinisellä ja QuickSort punaisella.


Lähdekoodi

# -*- coding: utf-8 -*-

import random, itertools

#Suorittaa yksittäisen ottelun
#Palauttaa tuplen, jossa (voittaja, häviäjä)
def pelaa_ottelu_sorted(joukkue1, joukkue2):
  if(random.gauss(joukkue1 - joukkue2, 0.5) < 0):
    return (joukkue2, joukkue1)
  else:
    return (joukkue1, joukkue2)

#Suorittaa lohkolle alkusarjan pelit
#Palauttaa tuplen, jossa lohkon joukkueet järjestettynä pisteiden mukaisesti
def pelaa_alkusarja_sorted(lohko):
  pistetaulu = [0]*8
  for (id1, jo1), (id2, jo2) in itertools.combinations(enumerate(lohko), 2):
    if pelaa_ottelu_sorted(jo1, jo2)[0] == jo2:
      pistetaulu[id2]+=3
    else:
      pistetaulu[id1]+=3

  #Järjestetään pisteiden perusteella
  return [x for (y, x) in sorted(zip(pistetaulu, lohko), reverse=True)]

#Toteuttaa turnauksen
#palauttaa neljä parasta joukkuetta, järjestettynä sijoituksen mukaan voittajasta aloittaen
def turnaus_sorted(joukkueet):

  lohkoA = pelaa_alkusarja_sorted(joukkueet[0:8])
  lohkoB = pelaa_alkusarja_sorted(joukkueet[8:18])

  puolivaliera1 = pelaa_ottelu_sorted(lohkoA[0], lohkoB[3])
  puolivaliera2 = pelaa_ottelu_sorted(lohkoA[2], lohkoB[1])
  puolivaliera3 = pelaa_ottelu_sorted(lohkoB[0], lohkoA[3])
  puolivaliera4 = pelaa_ottelu_sorted(lohkoB[2], lohkoA[1])

  valiera1 = pelaa_ottelu_sorted(puolivaliera1[0], puolivaliera2[0])
  valiera2 = pelaa_ottelu_sorted(puolivaliera3[0], puolivaliera4[0])

  finaali = pelaa_ottelu_sorted(valiera1[0], valiera2[0])
  pronssiottelu = pelaa_ottelu_sorted(valiera1[1], valiera2[1])

  #jarjestetaan lopullinen ranking
  tuloslista = finaali + pronssiottelu

  return tuloslista


joukkueet = range(16)
random.shuffle(joukkueet)
print "Kaikki joukkeet:", joukkueet
print "Neljä parasta:  ", turnaus_sorted(joukkueet)

Ei kommentteja:

Lähetä kommentti