How does the likelihood of winning a tennis match change as the likelihood of winning a single point changes? How about the probability of a best-of-five match ending in three sets? Let’s have some fun exploring some of these questions using a Python tennis match simulation program.
I won’t try to factor in all the parameters that affect a tennis match—no computer simulation can. The aim of this exercise is merely to experiment with a program written to provide an overview of object-oriented programming. You can follow the step-by-step tutorial leading to this program in Part 1: Simulating a Tennis Match Using Object-Oriented Programming in Python.
In Part 1, you wrote a program which keeps track of the scoring system in tennis. The program allows the user to record which player won each point and works out the score of the games, sets, and the match overall.
In Part 2—this article—you’ll automate this process so you can simulate a tennis match by allocating points randomly, using a point-winning likelihood that depends on the players’ ranking points.
The Tennis Simulation Program So Far
If you’ve just followed the tutorial in Part 1, then you can skip this section and move straight to the next one.
In Part 1, you created a number of classes to represent the key aspects of a tennis match. You created a Player
class and Match
, Set
, and Game
classes. The latter three inherited from a Unit
class that contains attributes and methods common to all three. Finally, you defined a Tiebreak
class which inherits from Game
.
You defined these classes in tennis.py
. Here’s the code so far:
# tennis.py class Player: def __init__(self, name="", ranking_points=0): self.name = name self.ranking_points = ranking_points def update_ranking_points(self, points_change): self.ranking_points += points_change def __str__(self): return self.name def __repr__(self): return ( f"Player(name='{self.name}', " f"ranking_points={self.ranking_points})" ) class Unit: def __init__(self, players=(Player(), Player())): self.players = players self.score = { self.players[0]: 0, # The key is of type Player self.players[1]: 0, } self.winner = None def get_winner(self): return self.winner def get_score(self): return self.score def is_running(self): return self.winner == None class Match(Unit): def __init__( self, player_1=Player(), player_2=Player(), best_of_5=True, ): super().__init__(players=(player_1, player_2)) self.best_of_5 = best_of_5 self.sets_to_play = 5 if best_of_5 else 3 self.sets = [] def play_set(self): set = Set(self, len(self.sets) + 1) self.sets.append(set) while set.is_running(): set.play_game() set_winner = set.get_winner() # Update set score for player who won set self.score[set_winner] += 1 # If player has won 2 sets if best-of-three # or 3 sets if best-of-five, match is over if self.score[set_winner] == self.sets_to_play // 2 + 1: self.winner = set_winner def play_match(self): while self.is_running(): self.play_set() print(f"\nWinner: {self.winner}") print(f"Score: {self}") def __str__(self): return "".join([str(set) for set in self.sets]) def __repr__(self): return ( f"Match(" f"player_1={self.players[0]}, " f"player_2={self.players[1]}, " f"best_of_5={self.best_of_5})" ) class Set(Unit): def __init__(self, match: Match, set_number=0): super().__init__(match.players) self.match = match self.set_number = set_number self.games = [] def play_game(self, tiebreak=False): # Creat a Game object and append to .games list if tiebreak: game = Tiebreak(self, len(self.games) + 1) else: game = Game(self, len(self.games) + 1) self.games.append(game) # Ask for user input to record who won point print( f"\nRecord point winner: " f"Press 1 for {self.players[0]} | " f"Press 2 for {self.players[1]}" ) while game.is_running(): point_winner_idx = ( int(input("\nPoint Winner (1 or 2) ->")) - 1 ) game.score_point(self.players[point_winner_idx]) print(game) # Game over - update set score self.score[game.winner] += 1 print(f"\nGame {game.winner.name}") print(f"\nCurrent score: {self.match}") # Check stage within set # If it's an early stage of the set and no one # reached 6 or 7 games, there's nothing else to do # and method can return if ( 6 not in self.score.values() and 7 not in self.score.values() ): return # Rest deals with latter stages of set when at least # one player is on 6 games # Check for 6-6 score if list(self.score.values()) == [6, 6]: self.play_game(tiebreak=True) return # …7-5 or 7-6 score (if tiebreak was played, score # will be 7-6) for player in self.players: # player reaches 7 games if self.score[player] == 7: self.winner = player return # player reaches 6 games # and 6-6 and 7-6 already ruled out if self.score[player] == 6: # Exclude 6-5 scenario if 5 not in self.score.values(): self.winner = player def __str__(self): return "-".join( [str(value) for value in self.score.values()] ) def __repr__(self): return ( f"Set(match={self.match!r}, " f"set_number={self.set_number})" ) class Game(Unit): points = 0, 15, 30, 40, "Ad" # Class attribute def __init__(self, set: Set, game_number=0): super().__init__(set.match.players) self.set = set self.game_number = game_number def score_point(self, player: Player): if self.winner: print( "Error: You tried to add a point to a completed game" ) return game_won = False current_point = self.score[player] # Player who wins point was on 40 if self.score[player] == 40: # Other player is on Ad if "Ad" in self.score.values(): # Update both players' scores to 40 for each_player in self.players: self.score[each_player] = 40 # Other player is also on 40 (deuce) elif list(self.score.values()) == [40, 40]: # Point winner goes to Ad self.score[player] = "Ad" # Other player is on 0, 15, or 30 else: # player wins the game game_won = True # Player who wins point was on Ad elif self.score[player] == "Ad": # player wins the game game_won = True # Player who wins point is on 0, 15, or 30 else: self.score[player] = Game.points[ Game.points.index(current_point) + 1 ] if game_won: self.score[player] = "Game" self.winner = player def __str__(self): score_values = list(self.score.values()) return f"{score_values[0]} - {score_values[1]}" def __repr__(self): return ( f"{self.__class__.__name__}(set={self.set!r}, " f"game_number={self.game_number})" ) class Tiebreak(Game): def __init__(self, set: Set, game_number=0): super().__init__(set, game_number) def score_point(self, player: Player): if self.winner: print( "Error: You tried to add a point to a completed game" ) return # Add point to player self.score[player] += 1 # Tiebreak over only if player has 7 or more points # and there's at least a 2 point-gap if ( self.score[player] >= 7 and self.score[player] - min(self.score.values()) >= 2 ): self.winner = player
You can test your code by creating a couple of players and a match, and playing the match. In Part 1 of this project, you created a script called play_tennis.py
:
# play_tennis.py from tennis_temp import Player, Match nadal = Player("Rafael Nadal", 2000) djokovic = Player("Novak Djokovic", 2000) test_match = Match(nadal, djokovic) test_match.play_match()
It’s now time to add an option to skip the manual recording of who won each point and let the program select a winner for each point.
Automating The Simulation Of The Tennis Match
The first step in creating a simulated tennis match is to automate the process which assigns each point to a player. You can start by randomly assigning points to players with an equal likelihood. Later, you can refine this to take into account the players’ ranking points.
You can create a new Boolean attribute in the Match
class called simulated
and a method which enables you to set it to True
if you want to simulate a match.
Then, you can use this flag to choose between the manual input of who wins each point and a random allocation. Parts of the code that haven’t changed are not shown below:
# tennis.py import random class Player:... class Unit:... class Match(Unit): def __init__( self, player_1=Player(), player_2=Player(), best_of_5=True, ): super().__init__(players=(player_1, player_2)) self.best_of_5 = best_of_5 self.sets_to_play = 5 if best_of_5 else 3 self.sets = [] self.simulated = False def simulate_match(self): self.simulated = True def play_set(self):... def play_match(self):... def __str__(self):... def __repr__(self):... class Set(Unit): def __init__(self, match: Match, set_number=0):... def play_game(self, tiebreak=False): # Creat a Game object and append to .games list if tiebreak: game = Tiebreak(self, len(self.games) + 1) else: game = Game(self, len(self.games) + 1) self.games.append(game) # Ask for user input to record who won point print( f"\nRecord point winner: " f"Press 1 for {self.players[0]} | " f"Press 2 for {self.players[1]}" ) # If match is simulated, assign points randomly, # otherwise, ask user to record who won each point while game.is_running(): if self.match.simulated: point_winner_idx = random.randint(0, 1) else: point_winner_idx = ( int(input("\nPoint Winner (1 or 2) ->")) - 1 ) game.score_point(self.players[point_winner_idx]) print(game) # Game over - update set score self.score[game.winner] += 1 print(f"\nGame {game.winner.name}") print(f"\nCurrent score: {self.match}") # Check stage within set # If it's an early stage of the set and no one # reached 6 or 7 games, there's nothing else to do # and method can return if ( 6 not in self.score.values() and 7 not in self.score.values() ): return # Rest deals with latter stages of set when at least # one player is on 6 games # Check for 6-6 score if list(self.score.values()) == [6, 6]: self.play_game(tiebreak=True) return # …7-5 or 7-6 score (if tiebreak was played, score # will be 7-6) for player in self.players: # player reaches 7 games if self.score[player] == 7: self.winner = player return # player reaches 6 games # and 6-6 and 7-6 already ruled out if self.score[player] == 6: # Exclude 6-5 scenario if 5 not in self.score.values(): self.winner = player def __str__(self):... def __repr__(self):... class Game(Unit):... class Tiebreak(Game):...
By default, simulated
is False
. However, you define the method simulate_match()
in Match
which changes simulated
to True
.
You use this flag in Set.play_game()
and, if it’s set to True
, you randomly choose a player to win each point.
You can test this addition to the code in play_tennis.py
by calling test_match.simulate_match()
:
# play_tennis.py from tennis import Player, Match nadal = Player("Rafael Nadal", 2000) djokovic = Player("Novak Djokovic", 2000) test_match = Match(nadal, djokovic) test_match.simulate_match() test_match.play_match()
You’ll no longer be required to select the winner of each point when you run this script. Instead, the computer program will choose each point’s winner. When you run this script, you’ll see the entire match being “played”, point-by-point. The output below is not shown in full:
Record point winner: Press 1 for Rafael Nadal | Press 2 for Novak Djokovic 15 - 0 30 - 0 40 - 0 Game - 0 Game Rafael Nadal Current score: 1-0 Record point winner: Press 1 for Rafael Nadal | Press 2 for Novak Djokovic 15 - 0 15 - 15 15 - 30 30 - 30 30 - 40 40 - 40 40 - Ad 40 - 40 40 - Ad 40 - Game Game Novak Djokovic Current score: 1-1 Record point winner: Press 1 for Rafael Nadal | Press 2 for Novak Djokovic 0 - 15 15 - 15 30 - 15 30 - 30 30 - 40 30 - Game Game Novak Djokovic Current score: 1-2 ... Current score: 3-6 6-3 4-6 7-6 5-5 Record point winner: Press 1 for Rafael Nadal | Press 2 for Novak Djokovic 15 - 0 30 - 0 30 - 15 30 - 30 30 - 40 40 - 40 Ad - 40 Game - 40 Game Rafael Nadal Current score: 3-6 6-3 4-6 7-6 6-5 Record point winner: Press 1 for Rafael Nadal | Press 2 for Novak Djokovic 15 - 0 30 - 0 30 - 15 40 - 15 40 - 30 40 - 40 40 - Ad 40 - 40 Ad - 40 40 - 40 Ad - 40 40 - 40 40 - Ad 40 - 40 40 - Ad 40 - Game Game Novak Djokovic Current score: 3-6 6-3 4-6 7-6 6-6 Record point winner: Press 1 for Rafael Nadal | Press 2 for Novak Djokovic 1 - 0 2 - 0 3 - 0 3 - 1 3 - 2 4 - 2 4 - 3 5 - 3 6 - 3 7 - 3 Game Rafael Nadal Current score: 3-6 6-3 4-6 7-6 7-6 Winner: Rafael Nadal Score: 3-6 6-3 4-6 7-6 7-6
The script simulates an entire match, point-by-point. The program still displays the instructions to record the winner of each point. You can place the call to print()
that displays this message in an if
statement so that it’s not shown when you’re running a simulated match:
# tennis.py # ... class Set(Unit): def __init__(self, match: Match, set_number=0):... def play_game(self, tiebreak=False): # Creat a Game object and append to .games list if tiebreak: game = Tiebreak(self, len(self.games) + 1) else: game = Game(self, len(self.games) + 1) self.games.append(game) # Ask for user input to record who won point if not self.match.simulated: print( f"\nRecord point winner: " f"Press 1 for {self.players[0]} | " f"Press 2 for {self.players[1]}" ) # If match is simulated, assign points randomly, # otherwise, ask user to record who won each point # ...
Assigning Points Based On Player Ranking Points
Let’s start by stating this again: no computer simulation can take account of all the factors involved in determining who wins a tennis match. I won’t even try. However, we can explore this a bit further.
At the moment, each point in the tennis match is assigned randomly with equal likelihood. You can improve a bit on this but taking into account the players’ ranking points and using those values to decide who’s more likely to win a point.
You can define a new ranking_ratio
attribute equal to the ranking points of the first player divided by the sum of ranking points of both players. Therefore, if the first player has 2000
ranking points and the second player has 1000
ranking points, then ratio will be 2000/(2000+1000)
, which is 0.667
.
Next, you can get the program to decide which player wins each point by using random.random()
to create a random number between 0
and 1
. If this random number is greater than ranking_ratio
, the first player wins the point. Otherwise, the second player wins the point.
You can make these additions to the code:
# tennis.py import random class Player:... class Unit: def __init__(self, players=(Player(), Player())): self.players = players self.score = { self.players[0]: 0, # The key is of type Player self.players[1]: 0, } self.winner = None self.ranking_ratio = self.players[0].ranking_points / ( self.players[0].ranking_points + self.players[1].ranking_points ) def get_winner(self): return self.winner def get_score(self): return self.score def is_running(self): return self.winner == None class Match(Unit):... class Set(Unit): def __init__(self, match: Match, set_number=0):... def play_game(self, tiebreak=False): # Creat a Game object and append to .games list if tiebreak: game = Tiebreak(self, len(self.games) + 1) else: game = Game(self, len(self.games) + 1) self.games.append(game) # Ask for user input to record who won point if not self.match.simulated: print( f"\nRecord point winner: " f"Press 1 for {self.players[0]} | " f"Press 2 for {self.players[1]}" ) # If match is simulated, assign points randomly, # otherwise, ask user to record who won each point while game.is_running(): if self.match.simulated: point_winner_idx = int( random.random() > self.ranking_ratio ) else: point_winner_idx = ( int(input("\nPoint Winner (1 or 2) ->")) - 1 ) game.score_point(self.players[point_winner_idx]) print(game) # Game over - update set score self.score[game.winner] += 1 print(f"\nGame {game.winner.name}") print(f"\nCurrent score: {self.match}") # Check stage within set # If it's an early stage of the set and no one # reached 6 or 7 games, there's nothing else to do # and method can return if ( 6 not in self.score.values() and 7 not in self.score.values() ): return # Rest deals with latter stages of set when at least # one player is on 6 games # Check for 6-6 score if list(self.score.values()) == [6, 6]: self.play_game(tiebreak=True) return # …7-5 or 7-6 score (if tiebreak was played, score # will be 7-6) for player in self.players: # player reaches 7 games if self.score[player] == 7: self.winner = player return # player reaches 6 games # and 6-6 and 7-6 already ruled out if self.score[player] == 6: # Exclude 6-5 scenario if 5 not in self.score.values(): self.winner = player def __str__(self):... def __repr__(self):... class Game(Unit):... class Tiebreak(Game):...
You add ranking_ratio
to the Unit
class which means that Match
, Set
, and Game
all have access to this attribute. In Set.play_game()
, you use the comparison operator >
to return True
or False
depending on whether a random number between 0
and 1
is greater than ranking_ratio
.
You could use the Boolean values directly as an index since True
and False
are equivalent to 1
and 0
. However, you use int()
to make this more explicit and readable.
The simulated match now accounts for the players’ ranking points and points are assigned in each game accordingly. You can try using different ranking points in play_tennis.py
and run the code several times to get a sense of how the match results change:
# play_tennis.py from tennis import Player, Match nadal = Player("Rafael Nadal", 2000) djokovic = Player("Novak Djokovic", 1000) test_match = Match(nadal, djokovic) test_match.simulate_match() test_match.play_match()
When I ran this script five times, I got the following results:
Winner: Rafael Nadal Score: 6-0 6-0 6-2 Winner: Rafael Nadal Score: 6-0 6-0 6-4 Winner: Rafael Nadal Score: 6-2 6-2 6-1 Winner: Rafael Nadal Score: 6-1 6-0 6-2 Winner: Rafael Nadal Score: 6-2 6-0 6-2
With the players having ranking points of 2000
and 1000
, the player with the higher ranking points now has a two-thirds likelihood of winning each point. This leads to Rafael Nadal winning all the five simulated matches comfortably in the example above.
Does this mean that Nadal will win all matches in three sets in this scenario? Is it possible for Djokovic to win any match at all?
Running five simulated matches is not sufficient to give you any certainty. You’ll need to run many more simulated matches. You’ll update your code to make this easy to do in the next section.
Running Many Simulated Matches
The first step to being able to run many simulated matches is to suppress the display of the results. You don’t want to print out thousands of lines showing the matches unfolding point-by-point.
You can add a display_results
flag and a method to set it to True
. Then, you can move all print lines in if
statements:
# tennis.py import random class Player:... class Unit:... class Match(Unit): def __init__( self, player_1=Player(), player_2=Player(), best_of_5=True, ): super().__init__(players=(player_1, player_2)) self.best_of_5 = best_of_5 self.sets_to_play = 5 if best_of_5 else 3 self.sets = [] self.simulated = False self.display_results = True def simulate_match(self):... def suppress_output(self): self.display_results = False def play_set(self):... def play_match(self): while self.is_running(): self.play_set() if self.display_results: print(f"\nWinner: {self.winner}") print(f"Score: {self}") def __str__(self):... def __repr__(self):... class Set(Unit): def __init__(self, match: Match, set_number=0): super().__init__(match.players) self.match = match self.set_number = set_number self.games = [] def play_game(self, tiebreak=False): # Creat a Game object and append to .games list if tiebreak: game = Tiebreak(self, len(self.games) + 1) else: game = Game(self, len(self.games) + 1) self.games.append(game) # Ask for user input to record who won point if not self.match.simulated: print( f"\nRecord point winner: " f"Press 1 for {self.players[0]} | " f"Press 2 for {self.players[1]}" ) # If match is simulated, assign points randomly, # otherwise, ask user to record who won each point while game.is_running(): if self.match.simulated: point_winner_idx = int( random.random() > self.ranking_ratio ) else: point_winner_idx = ( int(input("\nPoint Winner (1 or 2) ->")) - 1 ) game.score_point(self.players[point_winner_idx]) if self.match.display_results: print(game) # Game over - update set score self.score[game.winner] += 1 if self.match.display_results: print(f"\nGame {game.winner.name}") print(f"\nCurrent score: {self.match}") # Check stage within set # If it's an early stage of the set and no one # reached 6 or 7 games, there's nothing else to do # and method can return if ( 6 not in self.score.values() and 7 not in self.score.values() ): return # Rest deals with latter stages of set when at least # one player is on 6 games # Check for 6-6 score if list(self.score.values()) == [6, 6]: self.play_game(tiebreak=True) return # …7-5 or 7-6 score (if tiebreak was played, score # will be 7-6) for player in self.players: # player reaches 7 games if self.score[player] == 7: self.winner = player return # player reaches 6 games # and 6-6 and 7-6 already ruled out if self.score[player] == 6: # Exclude 6-5 scenario if 5 not in self.score.values(): self.winner = player def __str__(self):... def __repr__(self):... class Game(Unit):... class Tiebreak(Game):...
Now, you can write a loop in play_tennis.py
to run many simulated matches. The code stores how many times each player wins and how many times the match ends in three, four, or five sets:
# play_tennis.py from tennis import Player, Match n_simulations = 100 nadal = Player("Rafael Nadal", 2000) djokovic = Player("Novak Djokovic", 1000) winners = {nadal: 0, djokovic: 0} n_sets = {3: 0, 4: 0, 5: 0} for _ in range(n_simulations): match = Match(nadal, djokovic) match.simulate_match() match.suppress_output() match.play_match() print(match) winners[match.winner] += 1 n_sets[len(match.sets)] += 1 print(winners) print(n_sets)
You create two dictionaries called winners
and n_sets
to keep track of how many wins each player gets and how many sets each match has.
In the for
loop, you create and run a match, print out the result by calling print(test_match)
, and update the dictionaries. The output from this script is the following:
6-0 6-2 6-2 6-1 6-1 6-2 6-0 6-0 6-4 6-1 6-1 6-1 6-1 6-0 6-3 6-1 6-0 6-0 6-1 6-0 7-5 6-1 6-3 6-0 6-1 6-2 6-0 ... 6-1 6-1 6-0 6-2 7-6 6-0 6-2 7-5 6-1 6-0 6-0 6-0 6-0 6-1 6-0 6-0 6-0 6-1 6-0 6-1 6-1 6-2 6-0 6-0 6-0 6-0 6-2 {Player(name='Rafael Nadal', ranking_points=2000): 100, Player(name='Novak Djokovic', ranking_points=1000): 0} {3: 100, 4: 0, 5: 0}
I’ve truncated the output above to show only some of the match results. With the current ranking points for the players, Nadal wins all 100
simulated matches, and all end in three sets.
You can try to simulate even more matches. Although now, you don’t need to print each match result:
# play_tennis.py from tennis import Player, Match n_simulations = 100_000 nadal = Player("Rafael Nadal", 2000) djokovic = Player("Novak Djokovic", 1000) winners = {nadal: 0, djokovic: 0} n_sets = {3: 0, 4: 0, 5: 0} for _ in range(n_simulations): match = Match(nadal, djokovic) match.simulate_match() match.suppress_output() match.play_match() winners[match.winner] += 1 n_sets[len(match.sets)] += 1 print(winners) print(n_sets)
The results when running the simulation for 100,000
times are:
{Player(name='Rafael Nadal', ranking_points=2000): 100000, Player(name='Novak Djokovic', ranking_points=1000): 0} {3: 99606, 4: 393, 5: 1}
The gap in ranking points is large, and Nadal still wins all the matches under these simulation rules. However, some matches end up in four sets, and one is a five-setter. Note that these results will vary each time you run this simulation. You can experiment with different ranking points for the players.
Exploring How Ranking Points Affect Matches In The Simulations
You can extend the simulation to iterate through various combinations of ranking points for the players. To simplify, you can use ranking points in the range from 0
to 100
. You can start by printing out the results. Later, you’ll plot them. Note that you can use a smaller number for n_simulations
to speed up the code execution while you write your code:
# play_tennis.py from tennis import Player, Match n_simulations = 100_000 ranking_percentages = range(40, 61) for ranking_percentage in ranking_percentages: nadal = Player("Rafael Nadal", ranking_percentage) djokovic = Player("Novak Djokovic", 100-ranking_percentage) winners = {nadal: 0, djokovic: 0} n_sets = {3: 0, 4: 0, 5: 0} for _ in range(n_simulations): match = Match(nadal, djokovic) match.simulate_match() match.suppress_output() match.play_match() winners[match.winner] += 1 n_sets[len(match.sets)] += 1 print(f"\nRanking ratio: {match.ranking_ratio}") print(f"Player 1 winning percentage: {winners[nadal] / n_simulations * 100}%") print(f"Percentage of 3-set matches: {n_sets[3] / n_simulations * 100}%")
You iterate through a range of ranking point combinations and create Player
objects using those points. At the end of each iteration, you display the ranking ratio, the winning percentage for player 1, and the percentage of matches completed in three sets.
The output looks like this. I’m truncating the display below:
Ranking ratio: 0.4 Player 1 winning percentage: 0.058% Percentage of 3-set matches: 89.511% Ranking ratio: 0.41 Player 1 winning percentage: 0.14100000000000001% Percentage of 3-set matches: 84.905% Ranking ratio: 0.42 Player 1 winning percentage: 0.385% Percentage of 3-set matches: 79.225% Ranking ratio: 0.43 Player 1 winning percentage: 0.9979999999999999% Percentage of 3-set matches: 72.165% Ranking ratio: 0.44 Player 1 winning percentage: 2.2190000000000003% Percentage of 3-set matches: 63.757% ... Ranking ratio: 0.56 Player 1 winning percentage: 97.68299999999999% Percentage of 3-set matches: 63.359% Ranking ratio: 0.57 Player 1 winning percentage: 99.029% Percentage of 3-set matches: 71.846% Ranking ratio: 0.58 Player 1 winning percentage: 99.636% Percentage of 3-set matches: 79.091% Ranking ratio: 0.59 Player 1 winning percentage: 99.869% Percentage of 3-set matches: 84.76700000000001% Ranking ratio: 0.6 Player 1 winning percentage: 99.959% Percentage of 3-set matches: 89.562%
You’re now ready to plot the results using matplotlib
. You can install matplotlib
if you don’t have it already:
$ pip install matplotlib
Next, you can plot the results:
# play_tennis.py import matplotlib.pyplot as plt from tennis import Player, Match n_simulations = 100_000 n_player_1_wins = [] n_3_set_matches = [] ranking_percentages = range(40, 61) for ranking_percentage in ranking_percentages: nadal = Player("Rafael Nadal", ranking_percentage) djokovic = Player("Novak Djokovic", 100-ranking_percentage) winners = {nadal: 0, djokovic: 0} n_sets = {3: 0, 4: 0, 5: 0} for _ in range(n_simulations): match = Match(nadal, djokovic) match.simulate_match() match.suppress_output() match.play_match() winners[match.winner] += 1 n_sets[len(match.sets)] += 1 print(f"\nRanking ratio: {match.ranking_ratio}") n_player_1_wins.append(winners[nadal] / n_simulations * 100) print(f"Player 1 winning percentage: {n_player_1_wins[-1]}%") n_3_set_matches.append(n_sets[3] / n_simulations * 100) print(f"Percentage of 3-set matches: {n_3_set_matches[-1]}%") plt.style.use("Solarize_Light2") fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2) ax1.plot(ranking_percentages, n_player_1_wins) ax1.set_xlabel("Point win likelihood (%)") ax1.set_ylabel("Match win likelihood (%)") ax2.plot(ranking_percentages, n_3_set_matches) ax2.set_xlabel("Point win likelihood (%)") ax2.set_ylabel("3 set likelihood (%)") plt.show()
You create lists to hold the data generated by the simulations and append to these lists as you iterate through the different ranking points options.
To plot the results, you start by choosing the figure style you prefer. I’m using a solarized light style in this case, but you can use any other one you prefer of leave the line out to stick with the default option. You can see all style options using plt.style.available
.
You create a subplot with 1
row and 2
columns which returns a Figure
and two AxesSubplot
objects. These are classes defined in matplotlib
. You use the first AxesSubplot
object to plot the match-winning likelihood and the second for the three-set likelihood. This gives the following plot:
Final Words
It’s now up to you to explore other aspects of scoring in tennis and how different parameters affect the likelihood of winning.
Get the latest blog updates
No spam promise. You’ll get an email when a new blog post is published
The post Part 2: Simulating a Tennis Match Using Object-Oriented Programming in Python—Wimbledon Special appeared first on The Python Coding Book.