Sunday 7 July 2024

Great Expectations and how to manage them

Ever have this problem with expectations? Unreasonable, illogical expectations, to be exact.

It's almost inevitable if you are a developer working for non-tech people. Having little to no experience with the process of software development, they have no idea what constitutes a reasonable expectation, and let their imagination fill in the blanks. With amusing, and sometimes nightmarish, results. An (admittedly extreme) example would be the employer assuming you can fix the microwave simply because you know how to write software.

Fix this!

That's not to say experienced tech people don't have unreasonable expectations either. Their expectations tend to be a little less fanciful, but no less impractical when it comes right down to it. An example would be the employer expecting you, as an employee, to match his devotion to the company he created.

Then there are the in-betweeners. The ones who know a bit of programming, just enough to be problematic, but aren't exactly technical. Because they know how to code, they have expectations of how quickly code can be churned out. Unfortunately, they do not have experience with the less fun aspects of software development, such as best practices, testing and optimization. Therefore those expectations, too, can be divorced from reality.

People are like animals, in the sense that they need to be trained. Some might argue that humans have intelligence superior to animals. Well, that's great. That just makes it easier to train them. Make no mistake; where you are concerned, how people treat you is ultimately how you train them to treat you.

This kind of training tends to take two forms.

Don't give them what they want

And by that, I also mean stop being so obliging. When you satisfy an unreasonable request, it sets a new bar for expectations. And then it becomes the norm. This ultimately means that you're setting yourself up for increasing and unnecessary hardship.

If you get texts at night long after you're supposed to be off for the day, do not respond until the next day. Or at least, don't respond every single time. Or make them wait. Either way, make them expect that if they contact you at a certain time, they're not going to get a response right away, if at all.

If you're tasked to do something that is radically out of your job scope, make them expect you to fail. Sometimes spectacularly.

I've been known to work on weekends and public holidays. However, if my employers explicitly tell me that they expect me to use my weekends for work, I make damn sure I'll be doing something else during the weekend. It's the principle of the thing. It's not that I have a problem with working on weekends; I just have a problem with it being my employer's expectation. And, as an employee, I consider it my ethical responsibility to disabuse my employers, either in word or deed, of such expectations.

Gone fishing.

If the expectation is impossible, don't bust your ass trying to do it. Make sure you do the job that you're paid to do, and then maybe work on the impossible requirement. This sets the tone; you fulfil your professional obligations - no two ways about it, that's your first and foremost priority - and prevents expectations from getting too high. Once people see that they're not going to get what they asked for, but they are absolutely getting what they paid for, expectations get realigned. This, of course, assumes that you're dealing with reasonable people. In the corporate world, you don't always have that luxury. An alarming percentage of the time, you might end up looking for a new job.

Also, can't stress this enough, communication. Don't just say "yes" and then not do it - that's passive-aggressive bullshit and you should hold yourself to higher standards. Let them know it can't be done, why it can't be done, and what would have to give in order for you to carry it out.

Give them exactly what they want

Or what they say they want. People tend to tell you what they want without thinking it through. In order to be helpful, we should at least give them fair warning that what they want comes with a whole lot of caveats. And if that doesn't work, give them a preview of what fulfilling their wishes looks like.

And if that's still not enough, let them have it.

Now, just to be clear, I'm not advocating Malicious Compliance. It's one of the top fantasies of the average disgruntled employee (and understandably so!) but we're professionals. And as professionals, it is our duty to point out the pitfalls and advise accordingly. However, while we may be professionals, we're not superheroes. We can't save people from themselves. Thus, if an ill-advised route is insisted upon, short of it being illegal, give them what they want, and let the cards fall where they may.

They insist on not encrypting passwords and storing them in cleartext? Hey, go for it. But make sure the decision is reversible.

They insist on adding a new software feature that would compromise the functionality of existing features? Knock yourself out. Again, make sure you have a backup.

But if you go this route, document your objections. Obsessively. There may come a day when you need it. Also, plan your exit. People generally don't like being reminded of their failure, and your continued presence may be just such a reminder.

Here's a personal example. There was a time Mom had this unfortunate habit of calling me, and when I was otherwise occupied and didn't pick up, she would leave a string of missed calls on my phone. And then when I finally called her back, I would get an earful for not calling back right away. The kicker was, the body of her message was never anything that required immediate attention, such as an emergency. Like the house being on fire, or someone being sent to the hospital. No, the message would be something alone the lines of reminding me that Dad's birthday was next week, or asking if my refrigerator had any space for some extra apples she had picked up.

Missed calls.

Now, I love apples, but that's neither here nor there. I had to get her off this expectation that I had to be instantly responsive at all times regardless of the situation. The next time she called, I was walking alongside a busy road, and I had an idea. I picked up, and let her hear the blaring horns of irate road-users. She asked what the hell was going on there, and I innocently replied that I'd been in the process of crossing the road, and inquired what was the matter. In effect, I had given her a preview of just what could happen if she got what she wanted.

I wouldn't go so far as to say she never called me on my phone again, but each time she called, she had to weigh the contents of her message against the possibility of me getting run over by traffic just because I mindlessly picked up the phone as she demanded. The result was that she got into the habit of leaving messages on WhatsApp instead. You may think I'm a real asshole for scaring my own mother like that, but there was no lie in anything I did. There was a real possibility of something horrible happening if she got her way, and this was not a message that could be effectively delivered by me just telling her, since Asian parents, y'know, tend not to take their kids seriously.

On the flip side...

All this only works if you're competent at what you do, and you deliver what you're actually paid to do. It doesn't mean that just because you're bad at what you do, unreasonable requests become any less unreasonable. But being good at your actual job would help your case a lot more.

Expecting your response,
T___T

Tuesday 2 July 2024

The time is now, or at least, soon

Procrastination can be a beautiful thing. It staves off burnout, promotes peace of mind, slows life down to a manageable pace. And yet, taken too far, it can lead to regrets. Unfulfilled aspirations. Because time marches on, and doesn't come back. Opportunities are like origami boats floating along the river.

Like origami boats
in the water.

One of the challenges in life, in or outside the workplace, is to identify what things can be reasonably put off for another time, and what things would better serve you being done now. As in, now, pronto. Or at least, given a concrete timeline to begin.

For example, if you want to make a lifestyle change (Diet? Exercise?) putting it off indefinitely is a poor choice. Putting it off to next Monday or even the start of next month, however, can be justified on the grounds that it makes progress easier to track.

On the other hand, such justifications don't always pan out because, again, time marches on, and opportunities are lost.

A couple years back, I received a pay raise. It wasn't much; I was left with just over a hundred dollars extra after deductibles. I had the idea of giving the extra to my mother; after all, my pay had been going up the past couple years and she hadn't really benefitted from it. Then I looked at the pitiful amount and felt embarassed. Wouldn't it make more sense to wait another year, and if my pay went up again, put it all together to give her a more hefty income boost?

Running out of
time.

No. No, it wouldn't. The thing is, I was in my mid-forties and Mom was almost seventy. What if I waited another year and she croaked in the meantime? Sure, that hundred bucks wasn't going to make much of a splash. But I had given her to understand that whatever I got, we would share it. And it was time to deliver. The time was now. Or possibly never.

Even further back, in the year 2019, I was doing my usual annual appraisal of my current work situation and seeing if there were other opportunities out there. Part of me wanted to kick back and extend my contract by another year, and the other part insisted on at least attending a few interviews to see what was what.

Then COVID-19 happened. People got laid off left and right - generally I mean, not the software industry specifically. I decided that terrible though the entire thing was, it was an excellent opportunity. If I accepted that new job offer and it didn't work out, no one was going to question me not having a job, when just about the entire island was facing pretty much the same problem (though for different reasons). But I had to make the decision now. Not next three days, not next week. Now.

When COVID-19 wrecked
the world.

Spoiler alert: the new job didn't work out, but the resultant scenario happened just as I thought it would. I attended job interviews, and no one batted an eyelash when they saw I had that gap in my resume. I had successfully identified an opportunity in the midst of a crisis and taken advantage of it.

There have been several examples of this throughout my life, where I had to go do something. Before, it was a matter of finding a reason to do it; but at some point, it became a matter of finding a reason not to.

Take this blog, for example. When I started it back in 2014, did I wait till I was older and wiser and more professionally experienced before airing my views on a tech blog, for fear of embarrassing myself? No, I got off my ass and did it, public opinion be damned. It's now the tenth year I've been doing this. Has this blog become a resounding success? Also no. But it's a heck of a lot more successful than the blogs that never got started. And I've learned so much from doing this.

Just went out
there and started
this blog.

The "Now or Never" philosphy has been the driving force behind every new programming language I've learned, every tech platform I've tried. Every post-graduate Diploma course I've signed up for. All because I knew there would come a day my body just couldn't handle the rigors of studying while working any more.

Next time? There might not be a next time. And if I want to put things off, I have to be OK with that possibility.

The Time Is Now!

Inaction is a choice like any other, but bear in mind that all choices have an associated cost.

There's no need to jump into anything, of course. However, before deciding to do nothing, you might want to consider what you're missing out on, and if it's really worth it.

No time like the present.
T___T

Friday 28 June 2024

Remote workers in Dell Technologies stand firm

It was with a fair amount of interest that I read this article during the week. Computer company Dell Technologies, like many companies in the USA, has been trying to enforce a return to office for their workers, with dismal results.



During the worldwide crisis that was COVID-19, Dell mandated a remote work policy which their employees resisted at first, but gradually grew accustomed to. And now that COVID-19 is no longer life-threatening (at least for the general public), Dell has tried an about-face on this by demanding that employees return to the office.

Sound familiar?

If you're thinking that you've heard all this before, you probably have. This is a story that has been repeated over and over since 2021, not just in the USA but all over the world. Employers like Elon Musk have even stamped their metaphorical feet and opined that chosing to work from home is "immoral", which only serves to enforce my belief that being worth millions of dollars doesn't mean you're less prone to saying stupid shit.

Going to remote work meant that many employees had made certain commitments in terms of housing location and lifestyle. A return to office would disrupt everything, and incur certain costs such as transport, food and makeup. Nice shoes. The works. Those were just financial costs; there were also costs with regard to time, such as the daily commute.

Traffic is crazy.

Can we reasonably blame workers for not wanting to go through yet another drastic lifestyle change after going all in the first time? Should we accuse them all of being lazy and unmotivated or worse, actively trying to take advantage of the company? Those bad actors do exist, but does their existence justify tarring all remote workers with the same brush? Or is that just a convenient excuse?

Also, a company known for making computers portable and providing network access to those computers so that they can be used from anywhere, is insisting on a return to office? How's that for an unsubtle dose of irony?

Standoff

What's interesting is that Dell stated that those who opted to remain remote would no longer be considered for promotion... and a full fifty percent of remote employees didn't blink. The sentiment appears to be that the threat of not being promoted was an empty one, anyway, as opportunities for promotion had been scarce even before this. Their defiance seemed to state that even if there was a genuine chance of getting promoted had they chosen to acquiesce, promotion just wasn't worth making all those changes. The carrot wasn't working.

A chance at promotion?

Of course, the stick would soon follow. Or, if you consider the aforementioned "carrot" actually more of a stick, an even bigger stick would inevitably follow. The implication being that in the case of layoffs or company restructuring, remote workers would be the first on the chopping block.

Who's winning?

Well, arguably, the fact that this story is out in the public in the first place, means that Dell as a company has lost. And if Management goes to war with its own employees over this, Dell's reputation as a workplace can only fall further.

Resentment at Dell's machinations has caused employees to not only remain remote, but also to actively seek out new employment regardless of what Dell eventually decides. There's also the sneaky feeling all around, that Dell Management knows exactly what they're doing. After all, if those opting to stay remote really do leave, this eliminates the cost of compensating them if indeed layoffs do occur. To be fair to Dell, this tactic is hardly illegal. If we're being honest, it's not even exactly original.

Dell's tactics at this point in time would seem to be directly contradictory to the stand articulated by CEO Michael Dell less than two years ago. This is an excerpt from his blog, where he waxed lyrical about Dell's inclusive work culture.
But from my experience, if you are counting on forced hours spent in a traditional office to create collaboration and provide a feeling of belonging within your organization, you’re doing it wrong.

Technology’s ability to create a do-anything-from-anywhere world, where work is an outcome rather than a place or time, also enables you to create a strong corporate culture anywhere, all the time.


And another...
Ultimately, we have committed to allow team members around the globe to choose the work style that best fits their lifestyle - whether that is remote or in an office or a blend of the two. We are redesigning office spaces for the purpose of bringing teams together for social connection and collaboration to enhance hybrid experiences.

These decisions are grounded in our culture and based on the facts of our internal data. It is a philosophy of flexibility, choice, and connection.


Companies making a u-turn from their previous stands isn't at all new, of course... but a u-turn after that extravagant song and dance about "flexibility, choice, and connection"? Unless shame is a completely foreign concept to you, that's nothing short of an utter embarrassment.

Abrupt reversal in direction.

If we give Dell the benefit of the doubt, Dell may have been all about remote work in the beginning, but perhaps that sentiment was genuine until the tides of reality hit and they realized that they simply weren't that well-equipped for it. Perhaps this was an honest mistake.

Or perhaps, more cynically, it was just corporate gaslighting. Perhaps those words by Michael Dell were merely meant to sway workers to adopt remote work due to circumstances caused by COVID-19... and now that the pandemic is over, there's no longer any need for that pretense.

All in all...

As a salaried worker and an economic digit, my heart is with the employees of Dell no matter what their ultimate decisions are. This stalemate cannot end well; eventually the pendulum will have to swing one way or another. Only time will tell if Dell Technologies, as a company, can survive.

I would like to conclude by saying that I've never been a big fan of the USA. There's precious little about American culture that I admire, that I would choose over Asian culture. But now here's one thing, at least. I appreciate the way these workers at Dell have managed to prioritize the quality of their lives over corporate promotions. That's something that we in Southeast Asia, with our obsessive hustling and pointless flexing of empty job titles, can emulate.

What the Dell?!
T___T

Monday 24 June 2024

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

It's time to render the line chart!

Begin by using the figure() method to determine how large you want the chart to be. I'm going to recommend these proportions.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))


Now, we use the plot() method for line charts. We are going to first plot goals. Remember vals carries information for both goals and appearances. We want only goals right now.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"])


The marker argument defines what the points will look like. We want a circle, so I've input "o".
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o")


Finally, for goals, I want a nice solid scarlet, and that's what we'll have for the color attribute.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))


Finally, we use the show() method to render the chart.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))

  plt.show()


I chose to show the stats for Roberto Firminho, one of the longest-serving players in the menu. Notice that the data is rendered in circular dots and it's a bright red.


Now, we do the same for appearances! Only this time, we'll use a deeper shade of red.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  plt.show()


As you can see here, the scale has changed because the maximum number of appearances is vastly greater than the number of goals. (Still a great goalscorer though!)


Let's add these values to the plot. First, for goals, we want to iterate through the values using a For loop. Since we want to use the index value as well, we will need to use enumerate() on the values.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
  
  plt.show()


Then we use the text() method within the For loop. index determines the horizontal positioning, while value determines the vertical positioning. We'll add 1 to value so that the text won't overlap the dots. The next argument is the value itself, and we need to use str() on it so convert it to a string. Finally, we use the same color we did for plotting the values of the goals.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))
  
  plt.show()


Looks good.


Now we do the same thing for appearances!
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))

  
  plt.show()


I like the look of how things are shaping up!


Next, let's add lines to show average values. Since we have two sets of values, we want them in different colors too. Let's start with the average line for goals. We use the axhline() method for this. We begin by specifying that the y argument is the average of the goals properties in vals, using the namnean() method from numpy.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]))

  plt.show()


And then we make the line a dashed line by passing in "-." as the linestyle argument.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.")

  plt.show()


The color argument is obvious - it's the same as the plot color for goals.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))

  plt.show()


Nice!


We want to label it, so let's use the text() method. It starts at 0 for the horizontal position. For the vertical position, we want it just about the dashed line, so we use the same value as the dashed line, plus 5. The text will be "GOALS", and of course, we use the same color.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.show()


There it is.


Now, if you need me to elaborate on what you should do for appearances, I'll be terribly disappointed.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.axhline(y=np.nanmean(vals["appearances"]), linestyle="-.", color=(0.5, 0, 0))
  plt.text(0, np.nanmean(vals["appearances"]) + 5, "APPEARANCES", color=(0.5, 0, 0))

  
  plt.show()


We'll be done soon. The hard parts are over.


Now all that's left to do is use xlabel() and title() methods for your chart!
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.axhline(y=np.nanmean(vals["appearances"]), linestyle="-.", color=(0.5, 0, 0))
  plt.text(0, np.nanmean(vals["appearances"]) + 5, "APPEARANCES", color=(0.5, 0, 0))
  
  plt.xlabel("Seasons")
  plt.title("Liverpool FC Player Stats for " + player)

  plt.show()


Let's try it with Salah this time! Uh-oh, looks like the top label overlaps! This can happen if the stats for appearances are fairly consistent, putting the average value near the maximum.


So what we need to do is add this line, to set the highest limit to the max value for appearances, plus a few. This ought to be safe, because, barring some freak statistic, the number of appearances for any player over the course of a season should be significantly higher than the number of goals. Unless it's the kind of player who makes maybe five appearances in one season but scores a hat-trick each time. But this is too much of an anomaly for me to bother with right now.
def lineChart(labels, vals, player):
  plt.figure(figsize = (10, 5))

  plt.ylim(0, max(vals["appearances"]) + 10)
  plt.plot(labels, vals["goals"], marker="o", color=(1, 0, 0))
  plt.plot(labels, vals["appearances"], marker="o", color=(0.5, 0, 0))
  
  for index, value in enumerate(vals["goals"]):
    plt.text(index, value + 1, str(value), color=(1, 0, 0))

  for index, value in enumerate(vals["appearances"]):
    plt.text(index, value + 1, str(value), color=(0.5, 0, 0))
    
  plt.axhline(y=np.nanmean(vals["goals"]), linestyle="-.", color=(1, 0, 0))
  plt.text(0, np.nanmean(vals["goals"]) + 5, "GOALS", color=(1, 0, 0))
  
  plt.axhline(y=np.nanmean(vals["appearances"]), linestyle="-.", color=(0.5, 0, 0))
  plt.text(0, np.nanmean(vals["appearances"]) + 5, "APPEARANCES", color=(0.5, 0, 0))
  
  plt.xlabel("Seasons")
  plt.title("Liverpool FC Player Stats for " + player)
  plt.show()


Here we go.


Well, that was fun...

It's not all that often I do a web tutorial that doesn't involve coding for the web. But when I do, it's really rewarding. The matplotlib library seems pretty basic so far, but it has tons of features I'm itching to explore.

And that's the bottom line!
T___T

Friday 21 June 2024

Web Tutorial: Python Matplotlib Line Chart (Part 1/2)

Last month, we wrangled a bar chart in Python. This month, we will repeat the process, this time with a line chart.

Now, the considerations for a line chart are different from a bar chart. Line charts are primarily utilized to display progress over time. Therefore, the line chart will show both goals and appearances for particular players, over the course of several seasons.

There are some things we can reuse from the code, such as the dictionary from which we derive the data, and the helper function seasonName(). We will clear out the contents of the barChart() function, and rename it to lineChart(). This time, the function will not have season or stat as a parameter, because the line chart will show both goals and appearances across all seasons. We will, however, need the parameter player.

As for the rest of the code, we will discard it.
import numpy as np
import matplotlib.pyplot as plt

def lineChart(labels, vals, player):
  
def seasonName(year):
  return str(year) + "/" + str(year + 1)

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}
  },
  2018: {
    "Mohamed Salah": {"goals": 27, "appearances": 52},
    "Roberto Firminho": {"goals": 16, "appearances": 48},
    "Sadio Mane": {"goals": 26, "appearances": 50},
    "Alex Oxlade-Chamberlain": {"goals": 0, "appearances": 2}
  },
  2019: {
    "Mohamed Salah": {"goals": 23, "appearances": 48},
    "Roberto Firminho": {"goals": 12, "appearances": 52},
    "Sadio Mane": {"goals": 22, "appearances": 47},
    "Alex Oxlade-Chamberlain": {"goals": 8, "appearances": 43}
  },
  2020: {
    "Mohamed Salah": {"goals": 31, "appearances": 51},
    "Roberto Firminho": {"goals": 9, "appearances": 48},
    "Sadio Mane": {"goals": 16, "appearances": 48},
    "Alex Oxlade-Chamberlain": {"goals": 1, "appearances": 17},
    "Diogo Jota": {"goals": 13, "appearances": 30}
  },
  2021: {
    "Mohamed Salah": {"goals": 31, "appearances": 51},
    "Roberto Firminho": {"goals": 11, "appearances": 35},
    "Sadio Mane": {"goals": 23, "appearances": 51},
    "Alex Oxlade-Chamberlain": {"goals": 3, "appearances": 29},
    "Diogo Jota": {"goals": 21, "appearances": 55},
    "Luis Diaz": {"goals": 6, "appearances": 26}
  },
  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}
  }
}


We will require a variable, ans. That's the only input we will need.
  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}
  }
}
  
ans = ""


Therefore, when we create the While loop, this time we only need to do it as long as ans is not 0.
  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}
  }
}
  
ans = ""

while (ans != 0):


We will still need to have a menu. This time, we will have a menu of players. Create an empty list, players.
  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}
  }
}

players = []
  
ans = ""

while (ans != 0):


Then iterate through the data object. For every element, grab the list of players by using the keys() method, and add it to players.
players = []
for season in data:
  players = players + list(data[season].keys())

  
ans = ""


Now, since players reappear in different seasons, we are going to have duplicates in the list players, and we'll need to remove them. If we create a dictionary from the keys of players. this automatically eliminates all duplicates (dictionaries cannot have duplicates).
players = []
for season in data:
  players = players + list(data[season].keys())
  
dict.fromkeys(players)
  
ans = ""


Then we put the result back in a list using the list() function...
players = []
for season in data:
  players = players + list(data[season].keys())
  
list(dict.fromkeys(players))
  
ans = ""


... and assign the value back to players. So now the list players has all unique values.
players = []
for season in data:
  players = players + list(data[season].keys())
  
players = list(dict.fromkeys(players))
  
ans = ""


And then use the sort() method on players.
players = []
for season in data:
  players = players + list(data[season].keys())
  
players = list(dict.fromkeys(players))
players.sort()
  
ans = ""


Now within the While loop, we iterate through the players list using a For loop. Since we want the index value, we will need to use the enumerate() function on players.
players = list(dict.fromkeys(players))
players.sort()
  
ans = ""

while (ans != 0):
  for i, p in enumerate(players):


Thrn we'll print out the options as we iterate.
while (ans != 0):
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)


At the end of it, the option we print out is 0, to exit.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)
    
  print ("0: Exit")


And here, we ask the user to select from the list, using an input() function, and assign the value to ans.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)
    
  print ("0: Exit")

  ans = input("Select a player")


Before running the code, comment out the lineChart() function definition. We haven't written anything for it, so running the program will trigger an error.
#def lineChart(labels, vals, player):


There you go! A menu of players, nicely sorted in alphabetical order.


As before, we only want an integer value, so use the int() function here to force-convert the resultant value.
ans = int(input("Select a player"))


There's a possibility that the user will enter something non-numeric, so we encapsulate the entire thing in an infinite While loop and a Try-catch block.
while True:
  try:

    ans = int(input("Select a player"))
    break
  except:
    print("Invalid option. Please try again.")


After that, outside of the infinite While loop, we set the program to exit using the break statement if ans is 0. If the value of ans is numerical but invalid, we "restart" the While loop using the continue statement.
while True:
  try:
    ans = int(input("Select a player"))
    break
  except:
    print("Invalid option. Please try again.")  

if (ans == 0): break
if (ans > len(players) or ans < 0): continue


Let's try entering "hello". Obviously, it triggers the prompt ""Invalid option. Please try again."


Then we try a negative number. This "restarts" the While loop.


Then we try "0", and this quits the program altogether by exiting the outer While loop.


Now that we have a value, it's time to get other data. First, let's declare seasons as a list, within the first While loop.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)


Next, we define player by using the value of ans(minus 1 because counting starts from zero) as a reference to the players list. This will get you the name of the player chosen.
while (ans != 0):
  seasons = []
  
  for i, p in enumerate(players):
    print (str(i + 1) + ": " + p)
    
  print ("0: Exit")

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

  if (ans == 0): break
  if (ans > len(players) or ans < 0): continue
    
  player = players[ans - 1]


Then we declare stats as a dictionary object with goals and appearances as properties, both set to empty lists.
if (ans == 0): break
if (ans > len(players) or ans < 0): continue
  
player = players[ans - 1]
stats = { "goals": [], "appearances": []}


Now iterate through data using a For loop.
player = players[ans - 1]
stats = { "goals": [], "appearances": []}

for season in data:


We only want to operate on data that features the selected player. Thus we use an If block to check if player is in the list returned by running the keys() method on that current element in data.
for season in data:
  if player in data[season].keys():


If so, we append that particular season to seasons. But we'll want the proper season name instead, and that is where the seasonName() function comes in.
for season in data:
  if player in data[season].keys():
    seasons.append(seasonName(season))


We then add to stats. We want to append to the goals and appearances lists. Thus, if Luis Diaz was selected, the seasons 2021 and 2022 would be added to seasons. Then his goals and appearances for 2021 and 2022 would be appended to the goals and appearances properties of stats.
for season in data:
  if player in data[season].keys():
    seasons.append(seasonName(season))
    stats["goals"].append(data[season][player]["goals"])
    stats["appearances"].append(data[season][player]["appearances"])




Remember commenting out the call to lineChart()? Well, time to undo that action. We will next call lineChart() and use seasons, stats and player. seasons and player will be used to set labels and stats will provide values.
for season in data:
  if player in data[season].keys():
    seasons.append(seasonName(season))
    stats["goals"].append(data[season][player]["goals"])
    stats["appearances"].append(data[season][player]["appearances"])

lineChart(seasons, stats, player)


Next

Rendering the line chart with two sets of values.