Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22464

The Python Coding Blog: Part 2: Simulating a Tennis Match Using Object-Oriented Programming in Python—Wimbledon Special

$
0
0

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.


Viewing all articles
Browse latest Browse all 22464

Trending Articles