Table tennis home advantage analysis

And a festive bonus!

Posted by Lense Swaenen on July 21, 2021 · 23 mins read

English summary

As the most likely audience for this post are Flemish table tennis players, most of it is written in Dutch. Here a short summary:

I have played table tennis competitively for the last 15 years at table tennis club TTK Minderhout. Unfortunately a lot of visiting players do not like our accommodation and believe we have an unfair home advantage. All Flemish table tennis matches since season 2006-2007 are accessible through an open API. Analysing that data, shows that the average home advantage across all Antwerp clubs is about 5 percentage points (difference in win probability of home games versus out games). TTK Minderhout has no excessive home advantage whatsoever.

Heeft TTK Minderhout een uitzonderlijk thuisvoordeel?

Nee, is het antwoord.

Ik speel sinds 2002 tafeltennis bij tafeltennisclub TTK Minderhout. Na 4 jaar jeugdcompetitie, heb ik er nu ook een 15-tal jaren herencompetitie binnen de provincie Antwerpen opzitten, op een korte uitzwerving naar Ekerse na. TTK Minderhout huurt al zolang ik er speel tafeltennislokaal de noodkerk van het kerkfabriek. De speelzaal heeft een betonnen vloer, wat enige voordelen heeft inzake multifunctioneel gebruik van de zaal.

De meeste Antwerpse clubs spelen op een houten vloer of een sportvloer als in een turnzaal. In tafeltennis beinvloedt de ondergrond best wel de speelervaring, zoals in tennis gravel en gras dat ook doen (doch minder extreem in tafeltennis). Daar bezoekende ploegen en spelers onze vloer niet gewend zijn, en tafeltennisspelers een ras apart zijn, geeft dit regelmatig klagende en jammerende tegenstanders, wat voor ons als gastploeg al jaren een domper op de speelvreugde zet. Maar goed, misschien hebben ze gelijk. Vandaar dat ik tijdens de competitiestilte tijdens de coronacrisis eens de moeite heb genomen om na te rekenen wat de statistieken zeggen.

TabT API

Sinds seizoen 2006-2007 worden alle wedstrijden van de Vlaamse Tafeltennis Liga (VTTL) bijgehouden op https://competitie.vttl.be/. In de beginjaren was de URL iets als vttl.frenoy.net (URL werkt niet meer), naar de hoofdontwikkelaar Gaetan Frenoy. Tafeltennis was daarmee een van de eersten om zo uitgebreid digitaal te gaan. Badminton, squash, tennis zijn veel later gevolgd (badminton en squash met een toernooi.nl systeem, tennisvlaanderen met zijn eigen systeem). Na al die tijd vind ik het VTTL systeem het meest responsieve en gebruiksvriendelijke systeem, althans om snel en veel gedetailleerde resultaten te kunnen raadplegen. Ook de Franstalige vleugel en recreatieve federatie Sporcrea gebruiken ditzelfde systeem.

Gaetan Frenoy is zelfs zo goed geweest om een publieke SOAP API naar de database met spelers en uitslagen te voorzien. Deze heet de TabT API: http://tabt.frenoy.net/index.php?l=EN&display=MainPage_EN

Nu ben ik zelf geen held in web development en het ontrafelen van dergelijke APIs. Volgende website van TTC Erembodegem is voor mij daarom erg waardevol geweest om geautomatiseerd de nodige wedstrijd data te downloaden: Op http://ttc-erembodegem.be/tabtapi-test/ kan je bepaalde queries via een formulier doen. De broncode achter dit formulier is ook openbaar, namelijk: https://github.com/Laoujin/ttc-test-tabtapi

Om de SOAP API aan te roepen, is de zeep module een handige manier

import zeep

wsdl = 'https://api.vttl.be/?wsdl'
client = zeep.Client(wsdl=wsdl)
credentials = {'Account':'foo', 'Password':'bar'}
client.service.Test(credentials)
{
    'Timestamp': datetime.datetime(2021, 7, 21, 14, 12, 38, tzinfo=<FixedOffset '+02:00'>),
    'ApiVersion': '0.7.25',
    'IsValidAccount': False,
    'Language': 'nl',
    'Database': 'vttl',
    'RequestorIp': '2.15.139.88',
    'ConsumedTicks': 18,
    'CurrentQuota': 0,
    'AllowedQuota': 8000,
    'PhpVersion': None,
    'DbVersion': None
}

Wat je nog meer kan met niet-dummy credentials, weet ik niet. De response geeft ook wat aan van quota. Als we hier tegenaanlopen, dan doen we even een sleep en proberen we nog eens. Excuses Gaetan!

result = client.service.GetClubs()
print(result.ClubCount)
534

Een print van de hele result is zeer leerrijk, maar laat ik hier achterwege omwille van de lengte. Dit resultaat ziet er erg uit als een json dingetje. In praktijk is het een zeep object, dat eerder als een klasse met attributen aangeroepen moet worden dan een dict met keys.

result = client.service.GetSeasons()
print(result.CurrentSeason, result.CurrentSeasonName)
22 2021-2022

Seizoenen hebben naast een naam ook een integer label, wat gebruikt kan worden als argument van API calls (bvb. leden van een club in een bepaald seizoen).

Voor clubs is dan weer de clubcode als string nodig. Voor TTK Minderhout is deze A135

result = client.service.GetClubTeams(credentials, 'A135')
result.ClubName
'TTK Minderhout'

Thuisvoordeel TTK Minderhout

Als we GetMatches aanklikken op de TTC Erembodgemse test website, krijgen we signatuur van de GetMatches functie te zien. Deze ziet er zo uit: Request structure:

Array
(
	[Credentials] => Credentials Object
		(
			[Account] => 
			[Password] => 
		)

	[DivisionId] => 
	[Club] => 
	[Team] => 
	[DivisionCategory] => 
	[Season] => 
	[WeekName] => 
	[Level] => 
	[ShowDivisionName] => no
	[WithDetails] => 
	[MatchId] => 
)

Met deze API call trekken we alle competitieve ontmoetingen van TTK Minderhout in seizoen 2017-2018 binnen. Dit waren er 130.

club = 'A135'
result = client.service.GetMatches(credentials, None, club, None, None, 18)
print(result.MatchCount)
130

Een enkele ontmoeting ziet er dan zo uit:

result.TeamMatchesEntries[0]
{
    'DivisionName': None,
    'MatchId': 'PANTH01/014',
    'WeekName': '01',
    'Date': datetime.date(2017, 9, 15),
    'Time': datetime.time(20, 0),
    'Venue': 1,
    'VenueClub': 'A135',
    'VenueEntry': {
        'Id': None,
        'ClubVenue': None,
        'Name': 'TTC Minderhout (Noodkerk)',
        'Street': 'Schoolstraat',
        'Town': '2322 Minderhout',
        'Phone': None,
        'Comment': None
    },
    'HomeClub': 'A135',
    'HomeTeam': 'Minderhout A',
    'AwayClub': 'A139',
    'AwayTeam': 'Hallaar A',
    'Score': '13-3',
    'MatchUniqueId': 278412,
    'NextWeekName': '02',
    'PreviousWeekName': None,
    'IsHomeForfeited': False,
    'IsAwayForfeited': False,
    'MatchDetails': None,
    'DivisionId': 3324,
    'DivisionCategory': 1,
    'IsHomeWithdrawn': 'N',
    'IsAwayWithdrawn': 'N',
    'IsValidated': True,
    'IsLocked': True
}

Mijn houtje touwtje code hieronder parst de Score string en filtert bovendien de jeugdwedstrijden eruit op basis van de MatchID string.

home_games = 0
away_games = 0
home_wins = 0
home_loss = 0
away_wins = 0
away_loss = 0
for i, entry in enumerate(result.TeamMatchesEntries):
    if 'J' in entry.MatchId: # Filter jeugdwedstrijden er uit...
        continue
        
    if not entry.Score:
        continue
        
    home, away = entry.Score.split('-')
    try:
        home = int(home)
        away = int(away)
    except:
        continue
        
    if entry.HomeClub == 'A135':
        home_games += 1
        home_wins += home
        home_loss += away
        
    else:
        away_games += 1
        away_wins += away
        away_loss += home

print(home_games, away_games)
53 53

We tellen evenveel uitwedstrijden als thuiswedstrijden

print("%i, %i, %.3f, %i, %i, %.3f" % (home_wins, home_loss, home_wins/(home_wins + home_loss), away_wins, away_loss, away_wins/(away_wins + away_loss)))
476, 371, 0.562, 426, 422, 0.502

In dit ene seizoen heeft TTK Minderhout 476 individuele overwinningen thuis gehad, tegenover 371 nederlagen, goed voor een winstpercentage van 56%. Op verplaatsing heeft TTK Minderhout 426 overwinningen geboekt tegenover 422 nederlagen, goed voor een winstpercentage van 50%.

home_wins/(home_wins + home_loss) - away_wins/(away_wins + away_loss)
0.05962498050834242

In dit specifieke seizoen heeft TTK Minderhout een thuisvoordeel van 6 procentpunten gekend. Bovendien geen slecht seizoen voor de club met meer gewonnen dan verloren wedstrijden.

Laten we nu itereren over alle beschikbaren seizoenen:

divisionId = None
club = 'A135'
team = None
divisionCategory = None
for season in range(7, 22):
    result = client.service.GetMatches(credentials, divisionId, club, team, divisionCategory, season)

    home_games = 0
    away_games = 0
    home_wins = 0
    home_loss = 0
    away_wins = 0
    away_loss = 0
    for i, entry in enumerate(result.TeamMatchesEntries):
        if 'J' in entry.MatchId: # Jeugd...
            continue

        if not entry.Score:
            continue

        home, away = entry.Score.split('-')
        try:
            home = int(home)
            away = int(away)
        except:
            continue

        if entry.HomeClub == 'A135':
            home_games += 1
            home_wins += home
            home_loss += away

        else:
            away_games += 1
            away_wins += away
            away_loss += home
        #print(i, home, away)
    
    try:
        home_ratio = home_wins/(home_wins + home_loss)
        away_ratio = away_wins/(away_wins + away_loss)
    except:
        home_ratio = -1
        away_ratio = -1
        
    print("%i, %i, %i -- %i, %i, %.3f, %i, %i, %.3f, \t %.3f" % (season, home_games, away_games,
                                                        home_wins, home_loss, home_ratio, 
                                                        away_wins, away_loss, away_ratio, home_ratio - away_ratio))
7, 33, 33 -- 304, 200, 0.603, 319, 184, 0.634, 	 -0.031
8, 42, 42 -- 337, 335, 0.501, 284, 388, 0.423, 	 0.079
9, 60, 60 -- 683, 277, 0.711, 627, 333, 0.653, 	 0.058
10, 61, 61 -- 500, 474, 0.513, 446, 529, 0.457, 	 0.056
11, 58, 55 -- 476, 452, 0.513, 418, 462, 0.475, 	 0.038
12, 53, 51 -- 285, 563, 0.336, 212, 604, 0.260, 	 0.076
13, 42, 40 -- 282, 389, 0.420, 233, 407, 0.364, 	 0.056
14, 41, 41 -- 393, 263, 0.599, 347, 309, 0.529, 	 0.070
15, 41, 41 -- 367, 289, 0.559, 329, 327, 0.502, 	 0.058
16, 28, 29 -- 261, 187, 0.583, 273, 191, 0.588, 	 -0.006
17, 32, 32 -- 296, 216, 0.578, 273, 239, 0.533, 	 0.045
18, 53, 53 -- 476, 371, 0.562, 426, 422, 0.502, 	 0.060
19, 60, 60 -- 604, 356, 0.629, 536, 424, 0.558, 	 0.071
20, 52, 52 -- 409, 423, 0.492, 342, 489, 0.412, 	 0.080
21, 11, 12 -- 81, 95, 0.460, 82, 110, 0.427, 	 0.033

Het schommelt dus rond de 3 en 8 procentpunten, met een gemiddelde van 5.3.

Thuisvoordeel van alle Antwerpse clubs

De code hieronder genereert de lijst van alle Antwerpse clubs.

names = []
for i, club in enumerate(client.service.GetClubs().ClubEntries):
    if club.CategoryName != 'Antwerpen':
        continue
    
    if club.VenueCount == 0:
        continue
    
    clubId = club.UniqueIndex
    names.append(club.Name)
    
    print(i, clubId, club.Name)
1 A003 Salamander
2 A008 Brasgata
3 A062 AFP Antwerpen
4 A074 Hove
5 A075 Rupel
6 A095 Turnhout
7 A097 Nijlen
8 A105 Dessel
9 A115 Dylan Berlaar
10 A117 Geelse
11 A118 Rijkevorsel
12 A123 Virtus
13 A127 Retie
14 A129 Borsbeek
15 A130 Schoten
16 A135 Minderhout
17 A136 Zoersel
18 A138 Blue Rackets
19 A139 Hallaar
20 A141 Merksplas
21 A142 Tecemo
22 A147 Gierle
23 A155 Wommelgem
24 A159 Real
25 A160 Walem
26 A167 Lille
27 A176 Sokah
28 A182 Nodo
29 A186 Willebroek
30 A201 Hulshout
31 A211 Zwijndrecht
32 A212 Antonius
33 A216 Henricus
34 A218 Poppel
35 A219 Stari Bog

We hebben dus een drievoudige lus nodig: over alle clubs, alle seizoenen en dan alle gevonden wedstrijden:

import time

for i, club in enumerate(client.service.GetClubs().ClubEntries):
    if club.CategoryName != 'Antwerpen':
        continue
    
    if club.VenueCount == 0:
        continue
        
    clubId = club.UniqueIndex
    
    time.sleep(1.) # Sleep helpt met de quota niet te overschrijven / server niet te overbelasten
    
    divisionId = None
    team = None
    divisionCategory = None

    home_games = 0
    away_games = 0
    home_wins = 0
    home_loss = 0
    away_wins = 0
    away_loss = 0

    for season in range(15, 22):
        success = False
        while not success:
            try:
                result = client.service.GetMatches(credentials, divisionId, clubId, team, divisionCategory, season)
                success = True
            except Exception as e:
                print('fail', e)
                time.sleep(30.) # Sorry Gaetan!


        for i, entry in enumerate(result.TeamMatchesEntries):
            if 'J' in entry.MatchId: # Jeugd...
                continue

            if not entry.Score:
                continue

            home, away = entry.Score.split('-')
            try:
                home = int(home)
                away = int(away)
            except:
                continue

            if entry.HomeClub == clubId:
                home_games += 1
                home_wins += home
                home_loss += away

            else:
                away_games += 1
                away_wins += away
                away_loss += home
            #print(i, home, away)

    try:
        home_ratio = home_wins/(home_wins + home_loss)
        away_ratio = away_wins/(away_wins + away_loss)
    except:
        home_ratio = -1
        away_ratio = -1

    print("%4s, %20s -- %5i, %5i -- %5i, %5i, %.3f, %5i, %5i, %.3f, \t %.3f" % (clubId, club.Name, home_games, away_games,
                                                        home_wins, home_loss, home_ratio, 
                                                        away_wins, away_loss, away_ratio, home_ratio - away_ratio))

De volledige resultaten staan hieronder. De laatste kolom geeft het thuisvoordeel aan:

Full results:
A003,           Salamander --  1602,  1587 -- 14166, 11005, 0.563, 12523, 12440, 0.502, 	 0.061
A008,             Brasgata --  1478,  1458 -- 11294, 11959, 0.486, 10144, 12836, 0.441, 	 0.044
A062,        AFP Antwerpen --  2082,  2070 -- 16127, 15710, 0.507, 14521, 17122, 0.459, 	 0.048
A074,                 Hove --   660,   664 --  5976,  4522, 0.569,  5454,  5108, 0.516, 	 0.053
A075,                Rupel --   767,   762 --  6780,  5491, 0.553,  6121,  6071, 0.502, 	 0.050
A095,             Turnhout --   740,   716 --  6239,  5350, 0.538,  5506,  5699, 0.491, 	 0.047
A097,               Nijlen --   760,   762 --  5381,  6704, 0.445,  5175,  6956, 0.427, 	 0.019
A105,               Dessel --   150,   152 --  1084,  1217, 0.471,  1010,  1322, 0.433, 	 0.038
A115,        Dylan Berlaar --  1589,  1566 -- 12441, 11110, 0.528, 11000, 12213, 0.474, 	 0.054
A117,               Geelse --  1473,  1491 -- 12544, 10559, 0.543, 11367, 12036, 0.486, 	 0.057
A118,          Rijkevorsel --   391,   393 --  3608,  2648, 0.577,  3108,  3180, 0.494, 	 0.082
A123,               Virtus --   478,   471 --  3660,  3597, 0.504,  3150,  4006, 0.440, 	 0.064
A127,                Retie --   667,   662 --  5641,  4987, 0.531,  5070,  5476, 0.481, 	 0.050
A129,             Borsbeek --   589,   580 --  4814,  4609, 0.511,  4276,  5001, 0.461, 	 0.050
A130,              Schoten --  1282,  1274 -- 10391, 10120, 0.507,  9411, 10972, 0.462, 	 0.045
A135,           Minderhout --   667,   662 --  5754,  4890, 0.541,  5147,  5418, 0.487, 	 0.053
A136,              Zoersel --   955,   951 --  8436,  6485, 0.565,  7446,  7401, 0.502, 	 0.064
A138,         Blue Rackets --   540,   543 --  4201,  4347, 0.491,  3873,  4725, 0.450, 	 0.041
A139,              Hallaar --   840,   851 --  6883,  6457, 0.516,  6228,  7296, 0.461, 	 0.055
A141,            Merksplas --   460,   461 --  4167,  2663, 0.610,  3917,  2952, 0.570, 	 0.040
A142,               Tecemo --   687,   673 --  5844,  4913, 0.543,  5221,  5320, 0.495, 	 0.048
A147,               Gierle --  2094,  2034 -- 16930, 16306, 0.509, 14774, 17510, 0.458, 	 0.052
A155,            Wommelgem --   350,   354 --  3056,  2543, 0.546,  2707,  2955, 0.478, 	 0.068
A159,                 Real --    24,    21 --    44,    76, 0.367,    53,    52, 0.505, 	 -0.138
A160,                Walem --   421,   420 --  3320,  3416, 0.493,  3022,  3697, 0.450, 	 0.043
A167,                Lille --   457,   448 --  3642,  3643, 0.500,  3148,  4009, 0.440, 	 0.060
A176,                Sokah --  2077,  2082 -- 17004, 13898, 0.550, 15337, 15694, 0.494, 	 0.056
A182,                 Nodo --  1936,  1921 -- 14876, 13391, 0.526, 13485, 14640, 0.479, 	 0.047
A186,           Willebroek --   484,   485 --  4022,  3722, 0.519,  3390,  4370, 0.437, 	 0.083
A201,             Hulshout --   349,   354 --  3016,  2538, 0.543,  2954,  2679, 0.524, 	 0.019
A211,          Zwijndrecht --   407,   407 --  3801,  2645, 0.590,  3473,  2973, 0.539, 	 0.051
A212,             Antonius --   917,   920 --  7923,  6221, 0.560,  7271,  6968, 0.511, 	 0.050
A216,             Henricus --    94,    95 --   957,   547, 0.636,   858,   662, 0.564, 	 0.072

Merk op dat Real een nieuwe club is, waardoor zij nog maar weinig statistiek hebben en op een extreme waarde van -14 procentpunten uitkomen. In de visualisatie verderop negeren we hen. Andere kolommen waar we niet dieper op ingaan geven winstpercentages. We zien daar bijvoorbeeld hoe TTK Merkplas, die de laatste jaren in opmars is, met een percentage van 60 procent, bij de top hoort.

Wat sorteren en visualiseren:

home_advantage = [0.061,0.044,0.048,0.053,0.050,0.047,0.019,0.038,0.054,0.057,0.082,0.064,0.050,0.050,0.045,0.053,0.064,0.041,0.055,0.040,0.048,0.052,0.068,-0.138,0.043,0.060,0.056,0.047,0.083,0.019,0.051,0.050,0.072]

import numpy as np

names = np.array(names)
home_advantage = np.array(home_advantage)

inds = np.argsort(home_advantage)
%matplotlib notebook

import matplotlib.pyplot as plt

plt.figure(figsize=(6, 10))
plt.barh(np.arange(len(inds)-1), home_advantage[inds[1:]])
plt.barh([18], home_advantage[inds[18+1]], color='C1')
ax = plt.gca()
ax.set_yticks(np.arange(len(inds)-1))
ax.set_yticklabels(names[inds[1:]])
plt.title('Thuisvoordeel in procentpunten')
plt.tight_layout()
<IPython.core.display.Javascript object>

De cijfers tonen dus dat 5 procentpunten een typisch thuisvoordeel is, en dat TTK Minderhout geen uitzonderlijk thuisvoordeel heeft. De clubs met aan de extremen zijn overigens relatief kleine clubs met weinig officiele wedstrijden. Toevoeging van enige onzekerheidsbanden zou interessant zijn, maar is voor een andere keer. Mogelijks dat Salamander als grote club met een groot thuisvoordeel dat bovenaan komt, maar die zijn enkele jaren van zaal gewisseld, dus ook dat zou dan uitgespit moeten worden.

3e provinciale en hoger

Om nog net een beetje dieper te graven, heb ik bekeken of we met TTK Minderhout dan wel een significant thuisvoordeel zouden hebben als we enkel de hogere provinciale reeksen in beschouwing nemen. Misschien dat een bepaald speelniveau nodig is om een thuisvoordeel maximaal uit te buiten? De details heb ik uit de post gelaten, maar de conclusie van een analyse beperkt tot 1e, 2e en 3e provinciale was hetzelfde: Geen significant thuisvoordeel. Omdat data waarover we statistiek doen kleiner wordt, zien we wel dat de extremen groter worden, met Dylan Berlaar die tot 10 procentpunten thuisvoordeel in die reeksen behaalt.

Bonus: Vlaamse spelers met meeste individuele overwinningen in seniorencompetitie

In al de seizoenen dat ik bij Minderhout competitie heb gespeeld, heb ik er veel als kopman gefungeerd, waarbij ik in een lagere reeks speelde dan ik misschien zou aankunnen en hoge percentages (met regelmatige individuele leidersplaatsen in mijn reeks). Als dat geen clubliefde is!

Bovendien speel ik de meeste seizoenen bijna alle wedstrijden. Ik had dus het vermoeden dat er niet veel spelers in Vlaanderen meer individuele overwinningen in de enkelcompetitie zouden hebben dan ik. Met de TabT API werkend, de uitgelezen kans om dit eens te checken!

GetMembers API

De GetMembers API ziet er als volgt uit:

Array
(
	[Credentials] => Credentials Object
		(
			[Account] => 
			[Password] => 
		)

	[Club] => A135
	[Season] => 21
	[PlayerCategory] => 
	[UniqueIndex] => 
	[NameSearch] => 
	[ExtendedInformation] => 0
	[RankingPointsInformation] => 
)

Deze staat toe om individuele spelers op te vragen, inclusief al hun individuele resultaten.

Eerst vullen we een dictionary met alle spelers bij een club

clubInds = []
for club in clubs.ClubEntries:
    #if 'A' in club.UniqueIndex:
    clubInds.append(club.UniqueIndex)

club_to_members = {}
for club in clubInds:
    try:
        club_to_members[club] = client.service.GetMembers(credentials, club, 20, None, None, None, 0, None)
    except:
        continue

Vervolgens vullen we een dictionary voor elke speler met al zijn individuele resultaten over alle beschikbare seizoenen. Spelers hebben een unieke lidnummer. Vanuit mijn jaren als kapitein zijn toen wedstrijdladen nog op papier ingevuld moesten worden, ken ik die nog vanbuiten: 502575

member_to_results = {}

for club in club_to_members:
    #members = club_to_members[club]
    print(club)
    
    for season in range(7, 21):
        
        success = False
        tries = 0
        members = None
        while (not success) and (tries < 5):
            try:
                tries += 1
                members = client.service.GetMembers(credentials, club, season, None, None, None, 0, None, True)
                success = True
            except Exception as e:
                if not e.message.startswith('Quota'):
                    print(e.message)
                    members = None
                    success = True
                else: 
                    time.sleep(30.)
        
        if tries >= 5:
            print('Exceeded # of tries')
            
        if not members:
            continue
        
        assert out.MemberCount <= 1
        if out.MemberCount == 0:
            continue
            
        for member in members.MemberEntries:
            index = member.UniqueIndex
        
            if not index in member_to_results:
                member_to_results[index] = [0, 0] # Wins, losses
                
            results = member.ResultEntries
            for result in results:
                
                if not result.CompetitionType == 'C':
                    continue
                
                assert result.Result in 'VD'
                
                if result.Result == 'V':
                    member_to_results[index][0] += 1
                if result.Result == 'D':
                    member_to_results[index][1] += 1

Resultaten

Deze keer presenteren we het wat mooier in een pandas tabel

import pandas as pd

import copy

member_to_results2 = copy.deepcopy(member_to_results)

for club in club_to_members:        
    members = club_to_members[club].MemberEntries
    
    for member in members:
        index = member.UniqueIndex
        if not index in member_to_results2:
            continue
            
        if len(member_to_results2[index]) == 2:
            member_to_results2[index].append(member.FirstName)
            member_to_results2[index].append(member.LastName)

df = pd.DataFrame.from_dict(member_to_results2, orient='index', columns=['W', 'L', 'FirstName', 'LastName'])
df.sort_values('W', ascending=False)
W L FirstName LastName
500376 1039 731 JACQUES INGELBRECHT
502575 947 199 LENSE SWAENEN
506488 944 291 MICHEL MENTENS
503221 914 128 ROB WUYTS
506188 879 348 JAN LEBBE
505690 856 607 ANDRE AUDEZ
506952 832 352 TOMMY VOET
100577 829 334 FRANCOIS GOBEAUX
509041 828 358 JORNE BICKX
510800 824 204 JOHAN CARNA

72832 rows × 4 columns

Aanvoerder van bovenstaande lijst is Jacques Ingelbrecht, 70-jarige speler bij het West-Vlaamse Zandvoorde. Hij voert bovenstaande lijst aan met 1039 overwinningen op meer dan 1700 wedstrijden. De reden dat Jacques zoveel wedstrijden op zijn teller heeft, is omdat hij als veteraan ook al jaren meedraait in de West-Vlaamse veteranen competitie, zodat hij wekelijks, naast 4 wedstrijden seniorencompetitie ook nog eens 3 wedstrijden veteranencompetitie speelt. Deze had ik er niet uitgefilterd…

Als we die wedstrijden negeren (en toegegeven, zonder veteranen-, toernooi- en jeugdwedstrijden wordt het een beetje cherrypicking :)), dan voer ik weldegelijk de Vlaamse lijst aan van spelers met de meeste individuele overwinningen in seniorencompetitie! Joepie!

Verder nog een shout-out naar collega Antwerpenaren Rob Wuyts en Jan Lebbe die mee de top aanvullen. Tegen Rob en Jan heb ik de voorbije jaren al vele wedstrijden op het scherpst van de snee gespeeld en beide hebben duidelijk een even groot hart voor hun kleine club getoond (Hulshout en Hove respectievelijk).

Tot slot de observatie dat mijn teller van overwinningen de 1000 aan het naderen is! Dat had ik eerder nog niet op mijn radar, maar dit geeft me dus nog ongeveer 1 volledig seizoen de tijd om een gepaste viering voor die mijlpaal te plannen!

Verdere ideeen:

Nog enkele ideeen voor toekomstige analyses en blogposts die ik alvast even neerpen hier:

  • Toevoeging van onzekerheidsbanden aan de resultaten.
  • Uitnadeel ipv thuisvoordeel: Bij welke clubs presenteren bezoekers minder goed dan bij andere? En schept dit uberhaupt een ander beeld dan de vraag naar thuisvoordeel?
  • Wat voor verdelingen van matchscores (individueel dan wel op team niveau) vinden we terug? Wie zijn de belle-kampioenen en kneuzen?
  • Elo vs Glicko vs Trueskill rating systemen: Naast de officiele klassementen van de VTTL/KBTTB, kan je op de Frenoy-website ook Elo-ratings vinden. Het voorbije jaar heb ik me wat verdiept in verschillende ratingsystemen, zowel de praktische zoals die gehanteerd worden door de VTTL, Badminton Vlaanderen, Tennis Vlaanderen, Nederlandse tennisbond, etc. alsook de theoretisch gevestigde waarden als Elo, Glicko en Trueskill. Bij uitstek deze laatste is wiskundig erg interessant als toepassing van ‘expectation propagation’ type message passing voor inferentie op Bayesiaanse modellen. Dit heeft ook veel professionele relevantie voor mij, dus daar wil ik zeker nog een keer meer over schrijven. In elk geval, Trueskill heeft de belofte om sneller accuratere ratings te geven dan Elo, en onderbouwt dit onder meer met data uit online gaming. Interessant om een keer op al deze VTTL data te testen dan!

Als enige lezers nog suggesties hebben voor analyses, ben ik zeer geinteresseerd. Hopelijk dat de beschikbaarheid van de code in deze post ook andere in staat kan stellen om zelf wat interessants uit die grote hoeveelheid data op te graven.


None yet

Want to leave a comment?

Very interested in your comments but still figuring out the most suited approach to this. For now, feel free to send me an email.