Wednesday 15 May 2024

Web Tutorial: Python Matplotlib Bar Chart (Part 2/2)

Welcome back!

Before we proceed any further, we'll write a function that's going to be used later in multiple parts. You know how, so far, we have represented the seasons using year names? Well, that's not good enough. Mostly the English Premier League season starts around July and ends in May the following year. So the "2018" season would more accorately be the "2018/2019" season.

Let's write a function, seasonName(), to accept the season year as an argument and spit out the appropriate string.
def barChart(labels, vals, season, stat):
    plt.figure(figsize = (10, 5))

    plt.bar(labels, vals, color=(1, 0.2, 0.2))

    for index, value in enumerate(vals):
        plt.text(index - (len(str(value)) * 0.02), value + 1, str(value))

    plt.ylim(0, max(vals) + 5)
    plt.xticks(rotation=90)
    plt.axhline(y=np.nanmean(vals))

    plt.xlabel("Players")
    plt.ylabel("No. of " + stat)
    plt.title("Liverpool FC Player Stats for " + season)
    plt.show()
    
def seasonName(year):

data = {
    2017: {
        "Mohamed Salah": {"goals": 44, "appearances": 52},
        "Roberto Firminho": {"goals": 27, "appearances": 54},
        "Sadio Mane": {"goals": 20, "appearances": 44},
        "Alex Oxlade-Chamberlain": {"goals": 5, "appearances": 42}
    },


So we take season and convert it to a string using str(). We then concatenate "/" to it.
def seasonName(year):
    return str(year) + "/"


Finally, we concatenate the following year, also converted to a string via str().
def seasonName(year):
    return str(year) + "/" + str(year + 1)


Let's try this by changing the labels in barChart().
plt.xlabel("Players")
plt.ylabel("No. of " + stat)
plt.title("Liverpool FC Player Stats for " + seasonName(season))
plt.show()


We'll also need to slightly alter the function call to use the number 2018 instead of the string "2018".
barChart(players, stats, 2018, "goals")


So simple!


But the main reason we want this, is so that we can populate the user menu. We want a list of all seasons. For this, we use the keys() method on data and put the results in a list using the list() function. Then we assign the result to seasons. While we're at it, we prepare for the next operation by declaring ans1 and ans2 as empty strings.
    2022: {
        "Mohamed Salah": {"goals": 30, "appearances": 51},
        "Roberto Firminho": {"goals": 13, "appearances": 35},
        "Alex Oxlade-Chamberlain": {"goals": 1, "appearances": 13},
        "Diogo Jota": {"goals": 7, "appearances": 28},
        "Luis Diaz": {"goals": 5, "appearances": 21}
    }
}

seasons = list(data.keys())
    
ans1 = ""
ans2 = ""


players = list(data[2018].keys())
values = list(data[2018].values())

stats = [];
for v in values:
    stats.append(v["goals"])

barChart(players, stats, 2018, "goals")


Then use enumerate() on seasons so we can run it through a For loop with an index, i. We want to print i, but of course we have to convert it to a string via the str() function.
seasons = list(data.keys())
    
ans1 = ""
ans2 = ""

for i, s in enumerate(seasons):
    print (str(i + 1))

        
players = list(data[2018].keys())
values = list(data[2018].values())


Then concatenate ":" and a space, and the season.
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))

players = list(data[2018].keys())
values = list(data[2018].values())


Cap it off with one last option, 0.
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))
    
print ("0: Exit")
        
players = list(data[2018].keys())
values = list(data[2018].values())


There, you see the entire list appears on the menu!


We want the user to pick a season. Thus, we use the input() function, passing in a user-friendly prompt as an argument. We then set the result to ans1.
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))
    
print ("0: Exit")

ans1 = input("Select a season")

players = list(data[2018].keys())
values = list(data[2018].values())


There you see the prompt appear. The bar chart will not appear until a value is entered.


But hold on... we want a number to be entered. How do we enforce this? We can force convert the value using the int() function. However, this is going to throw an error if someone enters something like "test".
for i, s in enumerate(seasons):
    print (str(i + 1) + ": " + seasonName(s))
    
print ("0: Exit")

ans1 = int(input("Select a season"))
        
players = list(data[2018].keys())
values = list(data[2018].values())


To forestall that, we use a Try-catch block. So if getting that input triggers an exception, an error message is printed.
print ("0: Exit")

try:
    ans1 = int(input("Select a season"))
except:
    print("Invalid option. Please try again.")

        
players = list(data[2018].keys())
values = list(data[2018].values())


We wrap this entire segment up in an infinite While loop, and add a break statement after running input(). Thus, if the input is invalid, the program keeps prompting the user until it gets an acceptable answer. Once the answer is valid, the break statement exits the While loop.
print ("0: Exit")

while True:
    try:
        ans1 = int(input("Select a season"))
        break
    except:
        print("Invalid option. Please try again.")    
        
players = list(data[2018].keys())
values = list(data[2018].values())


Try it. Enter an invalid value.


An error message will be printed, and you'll get the prompt again until you enter in a number.


From here, ans1, minus 1, will be used as an index referencing a value in the seasons array, and the result is season. And instead of 2018, we make sure the players and values now uses season.
while True:
    try:
        ans1 = int(input("Select a season"))
        break
    except:
        print("Invalid option. Please try again.")    
        
season = seasons[ans1 - 1]

players = list(data[season].keys())
values = list(data[season].values())


Finally, we make sure that season is passed into barChart() instead of 2018.
players = list(data[season].keys())
values = list(data[season].values())

stats = [];
for v in values:
    stats.append(v["goals"])

barChart(players, stats, season, "goals")


This is what happens when you enter in "4". It will look for the the element in seasons array at index position 3, resulting in the value for Season 2020/2021. And then the bar chart is generated!


We still need to tidy this up a little, though. What happens when the user enters a 0? What if they enter a number, but it's out of the desired range? For this, we encase the entire thing in yet another While loop that will run as long as ans1 or ans2 is not 0.
while (ans1 != 0 and ans2 != 0):    
    for i, s in enumerate(seasons):
        print (str(i + 1) + ": " + seasonName(s))
        
    print ("0: Exit")

    while True:
        try:
            ans1 = int(input("Select a season"))
            break
        except:
            print("Invalid option. Please try again.")    
        
    season = seasons[ans1 - 1]
    
    players = list(data[season].keys())
    values = list(data[season].values())

    stats = [];
    for v in values:
        stats.append(v["goals"])

    barChart(players, stats, season, "goals")


Then we handle the case for if the user enters a 0. In this case, we break out of the While loop using a break statement.
while (ans1 != 0 and ans2 != 0):    
    for i, s in enumerate(seasons):
        print (str(i + 1) + ": " + seasonName(s))
        
    print ("0: Exit")

    while True:
        try:
            ans1 = int(input("Select a season"))
            break
        except:
            print("Invalid option. Please try again.")    

    if (ans1 == 0): break
        
    season = seasons[ans1 - 1]
    
    players = list(data[season].keys())
    values = list(data[season].values())

    stats = [];
    for v in values:
        stats.append(v["goals"])

    barChart(players, stats, season, "goals")


If the numeric value of ans1 is out of the valid range of values, we use a continue statement to "restart" the While loop.
while (ans1 != 0 and ans2 != 0):    
    for i, s in enumerate(seasons):
        print (str(i + 1) + ": " + seasonName(s))
        
    print ("0: Exit")

    while True:
        try:
            ans1 = int(input("Select a season"))
            break
        except:
            print("Invalid option. Please try again.")    

    if (ans1 == 0): break
    if (ans1 > len(seasons) or ans1 < 0): continue
        
    season = seasons[ans1 - 1]
    
    players = list(data[season].keys())
    values = list(data[season].values())

    stats = [];
    for v in values:
        stats.append(v["goals"])

    barChart(players, stats, season, "goals")


See this? I first entered -6, which is out of the valid range, and it re-prompted me to make a choice. Same thing happened when I entered 8. But once I entered 0, the program stopped running.


Let's go ahead and do some user input for the stat type. So far we've been hard-coding "goals" as the statistic. Well, that's gonna change. Print the following lines.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")


players = list(data[season].keys())
values = list(data[season].values())


Then repeat what we did for ans1, except this time we check ans2, and use the input() function to request for a stat.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")

while True:
    try:
        ans2 = int(input("Select a stat"))
        break
    except:
        print("Invalid option. Please try again.")


players = list(data[season].keys())
values = list(data[season].values())


Again, we repeat what we did for ans1 for ans2. The same logic applies here.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")

while True:
    try:
        ans2 = int(input("Select a stat"))
        break
    except:
        print("Invalid option. Please try again.")

if (ans2 == 0): break
if (ans2 > 2 or ans2 < 0): continue


players = list(data[season].keys())
values = list(data[season].values())


Then we handle the other options. If the user has entered 1, we set the variable stat to "goals". If the user has entered 2, we set stat to appearances. We won't have to bother declaring stat beforehand because in any other case, the program either ends or "restarts" the While loop.
if (ans1 == 0): break
if (ans1 > len(seasons) or ans1 < 0): continue
    
season = seasons[ans1 - 1]

print ("1: goals")
print ("2: appearances")
print ("0: Exit")

while True:
    try:
        ans2 = int(input("Select a stat"))
        break
    except:
        print("Invalid option. Please try again.")

if (ans2 == 1): stat = "goals"
if (ans2 == 2): stat = "appearances"

if (ans2 == 0): break
if (ans2 > 2 or ans2 < 0): continue

players = list(data[season].keys())
values = list(data[season].values())


In the For loop, we replace the hard-coded value of "goals" with stat.
players = list(data[season].keys())
values = list(data[season].values())

stats = [];
for v in values:
    stats.append(v[stat])

barChart(players, stats, season, "goals")


And also in the barChart() function call.
players = list(data[season].keys())
values = list(data[season].values())

stats = [];
for v in values:
    stats.append(v[stat])

barChart(players, stats, season, stat)


We repeat the tests. If we enter "hello" for the stat, it repeats the prompt.


If we enter an invalid range, it "repeats" the While loop.

Here, we enter 2 for "appearances"... and the chart generates! You can see that the label has changed from "goals" to "appearances" as well.


That's it...

This is but the tip of the iceberg. Python's matplotlib library offers a whole lot of functionality for customizing bar charts. Go check it out!

Luis Diaz, Mohamed Salah and Diogo Jota walk into a bar...
T___T

No comments:

Post a Comment