Updates to Simulation Graphics
10 min read

Updates to Simulation Graphics

Updates to Simulation Graphics

I have a list of things in my head that as time permits I like to try to work on.

Some of them are yeah that would be nice and are probably more projects for summer, these are things like looking at how many yards players/teams gain from throw ins (I have always wondered who was the best at stealing yards with the sneaky walking about the line)

Some are things that I want to write about and need to make changes to how I handle data or how my graphics work, this is something like wanting to add additional seasons and weighting to my rating systems (I really want to do this for the strikers before I write about it but we will see how that goes).

Some are things that I created a while ago and my style and skills have changed since I initially created them and I want to update them to match with how things look now. A good example of that is what I am going to talk about today with my graphics that accompany my simulation model.

It is not that these looked bad per se but there was room for improvement. The first is the change in the distribution charts.

Original:

Updated:

The changes here aren't huge but I think they do make things much easier to read and understand. I found that reading the team names was hard, so I made the names bigger and black, I also removed the average rank because I don't think that really added any value and was a kludge that I settled on when I first created things to get the names to come out correct.

The other changes included adding average points and a few gridlines to help your eye track the points scale on the bottom. Plus a bit of branding and update to my house font style.

Original:

Updated:

This one is a much bigger change than the first one. The original is simply a table that I created from Excel. It is fine for presenting the data, especially because that is still where the simulation data lives (moving that to python is probably going to be a summer project).

I took some inspiration from how FiveThirtyEight presents their data and decided that adding a bit more information that feeds into the simulation model would be helpful as well.

The biggest changes here are the additions of the model's team rating scores and some cool color changes that correspond with the odds and rankings for each team. Also, some updates so that it uses the correct font and some additional branding.

How this works in python

If you're curious how I did this in python I will share some of the code here. Everything is available on my Github:

crabstats/joyplot.ipynb at main ยท ohthatcrab/crabstats
image files. Contribute to ohthatcrab/crabstats development by creating an account on GitHub.

To create the ride plot I use the package joypy and the results of my simulation. The simulation data is a summary of how each team did with Goals, Goals Against, Goal Difference, Points, Wins, Losses, Draws, and finishes for each of the 10,000 simulated seasons.

From there the first step is creating the figure and then setting things up for the joyplot/ridgeplot:

plt.figure(dpi=380)
fig, ax = joypy.joyplot(df
, column=['Pts','Team']
, overlap=0.0
, by="Overall"
, ylim='own'
, x_range=(10,105) #points scale
, fill=True
, figsize=(15,13)
, legend=False
, xlabels=True
, ylabels=False
#, color=['#76a5af', '#134f5c']
, colormap=lambda x: color_gradient(x, start=(.08, .45, .8)
,stop=(.8, .34, .44))
, alpha=0.6
, linewidth=.5
, linecolor='w'
#, background='k' # change to 'k' for black background or 'grey' for grey
, fade=True)

Next to create some more space around the plot to be able to add titles, endnote credits and team names:

plt.subplots_adjust(left=0.15,
bottom=0.12,
right=0.92,
top=0.88,
)

Next is add in a bit of information on the axis, invert the y-axis so that the teams that have more points are on top, and adjust the standard font information:

plt.rc("font", size=20,)
plt.gca().invert_yaxis()
plt.xlabel('Points', fontsize=30, color='grey', fontproperties=font_normal.prop)

fig.text(.5,0.9,f"Simulated Points Distribution",size=50, ha='center',fontproperties=font_bold.prop)
fig.text(.97,0.05,'@oh_that_crab', size=25, color='#000000', ha='right', fontproperties=font_normal.prop)

Next is creating some summary dataframes and lists to use later.

dfavg = df.groupby('Team').mean()
dfavg = dfavg.sort_values('Pts', ascending=False)
index = dfavg.index
a_list = list(index)
pts = dfavg.Pts
c_list = list(pts)
b_list = [round(num, 1) for num in c_list]

Next is to start adding the team names and points to the plot with a loop and the lists we created above.

x=0
for i in a_list:
fig.text(0.14,(0.845-(x0.038)),a_list[x], size=18, color='#000000', ha='right', fontproperties=font_normal.prop)
fig.text(0.94,(0.845-(x0.038)),b_list[x], size=18, color='#000000', ha='center', fontproperties=font_normal.prop)
x= x+1

Next is to put in the lines for the grids, I am using custom lines because I didn't love the way that the other ones looked.

fig.add_artist(lines.Line2D([0.233, 0.233], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=1, alpha=0.5))
fig.add_artist(lines.Line2D([0.313, 0.313], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=0.5, alpha=0.5))
fig.add_artist(lines.Line2D([0.393, 0.393], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=1, alpha=0.5))
fig.add_artist(lines.Line2D([0.474, 0.474], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=0.5, alpha=0.5))
fig.add_artist(lines.Line2D([0.555, 0.555], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=1, alpha=0.5))
fig.add_artist(lines.Line2D([0.6355, 0.6355], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=0.5, alpha=0.5))
fig.add_artist(lines.Line2D([0.716, 0.716], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=1, alpha=0.5))
fig.add_artist(lines.Line2D([0.7965, 0.7965], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=0.5, alpha=0.5))
fig.add_artist(lines.Line2D([0.877, 0.877], [0.122, 0.87], c='grey', linestyle='dotted', linewidth=1, alpha=0.5))

Last before we save things is to add the crab stats logo and a put of explanation text.

ax_cs_logo = add_image(cs_circ, fig, left=0.9, bottom=0.875, width=0.065)
fig.text(.02,0.04,'The distribution of points from 10,000 simulations\nExplanation: https://www.crabstats.xyz/explaining-my-projection-model',
size=14, color='#000000', ha='left',fontproperties=font_normal.prop)

plt.savefig('Simulations/Premier League Simulation - ' + date + '.png', bbox_inches='tight')

This is basically how to go about creating the distribution plots. Next is how this works to create the table style presentation. This could probably use some clean up and optimising but it runs fast enough for me where I don't really care if I added a few more lines and created things that I didn't really need. This is where my not a computer science background shows.

First load the data and create some lists to use later:

dfpts = pd.read_csv(r'sim.csv', encoding='utf-8', error_bad_lines=False,skip_blank_lines=True)
dfpts = dfpts.sort_values('Points', ascending=False)
tm_list = list(dfpts['Squad'])
tm_rate = list(dfpts['Team Rating'])
tm_pts = list(dfpts['Points'])
tm_gd = list(dfpts['GD'])
tm_title = list(dfpts['Title'])
tm_chmps = list(dfpts['Champions League'])
tm_el = list(dfpts['Europa League'])
tm_crl = list(dfpts['Confrence League'])
tm_rel = list(dfpts['Relegation'])
tm_rate_a = list(dfpts['Attack'])
tm_rate_d = list(dfpts['Defense'])
tm_rate_o = list(dfpts['Team Strength'])

Next create a plot and do some initial set up:

plt.style.use('fivethirtyeight')
fig = plt.figure(figsize=(20,14), tight_layout=True)
ax = fig.add_subplot(111)
ax.grid(False)
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)

Next is the area that could use a touch of cleanup but this initial method here gave me some flexibility in moving stuff around to how I wanted them to look. Some of the other things I did here were to add a variable for the transparency to give that nice look for the table, the other was a few if statements for the rankings colors:

x=0
for i in tm_list:
fig.text(0.05,(0.81-(x0.03775)),tm_list[x], size=25, color='#000000', ha='left', fontproperties=font_bold.prop)
x= x+1
x=0
fig.text(0.21,0.85,'Points', size=25, color='#000000', ha='center', fontproperties=font_bold.prop)
for i in tm_pts:
fig.text(0.21,(0.81-(x0.03775)),int(tm_pts[x]), size=25, color='#000000', ha='center', fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.26,0.85,'Goal\nDiff', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_gd:
fig.text(0.26,(0.81-(x0.03775)),int(tm_gd[x]), size=25, color='#000000', ha='center', fontproperties=font_normal.prop)
x= x+1
fig.add_artist(lines.Line2D([0.30, 0.30], [0.095, 0.83], c='grey', linestyle='dotted', linewidth=2, alpha=0.5))
x=0
fig.text(0.34,0.85,'Title', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_title:
s = int(round(tm_title[x]100,0))
if tm_title[x]+0.2 > 1:
a = 0
else:
a=0.2
fig.text(0.34,(0.81-(x0.03775)),f'{s}%', size=25, color='#000000', ha='center', alpha= tm_title[x]+a, fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.41,0.85,'Champions\nLeague', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_chmps:
s = int(round(tm_chmps[x]100,0))
if tm_chmps[x]+0.25 > 1:
a = 0
else:
a=0.25
fig.text(0.41,(0.81-(x0.03775)),f'{s}%', size=25, color='#000000', ha='center', alpha= tm_chmps[x]+a, fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.4925,0.91,'Simulated Probabilities', size=27, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
fig.add_artist(lines.Line2D([0.33, 0.66], [0.905, 0.905], c='grey', linestyle='dotted', linewidth=2, alpha=0.5))
fig.text(0.4925,0.85,'Europa\nLeague', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_el:
s = int(round(tm_el[x]100,0))
if tm_el[x]+0.3 > 1:
a = 0
else:
a=0.3
fig.text(0.4925,(0.81-(x0.03775)),f'{s}%', size=25, color='#000000', ha='center', alpha= tm_el[x]+a, fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.57,0.85,'Conference\nLeague', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_crl:
s = int(round(tm_crl[x]100,0))
if tm_crl[x]+0.4 > 1:
a = 0
else:
a=0.4
fig.text(0.57,(0.81-(x0.03775)),f'{s}%', size=25, color='#000000', ha='center', alpha= tm_crl[x]+a, fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.65,0.85,'Relegated', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_rel:
s = int(round(tm_rel[x]100,0))
if tm_rel[x]+0.3 > 1:
a = 0
else:
a=0.3
fig.text(0.65,(0.81-(x0.03775)),f'{s}%', size=25, color='#000000', ha='center', alpha= tm_rel[x]+a,fontproperties=font_normal.prop)
x= x+1
fig.add_artist(lines.Line2D([0.7, 0.7], [0.095, 0.83], c='grey', linestyle='dotted', linewidth=2, alpha=0.5))
x=0
fig.text(0.75,0.85,'Overall\nRating', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_rate:
if tm_rate[x]> 130:
c = 'darkgreen'
elif tm_rate[x]> 110:
c = 'mediumseagreen'
elif tm_rate[x]> 90:
c = 'black'
elif tm_rate[x]> 70:
c = 'indianred'
else:
c = 'darkred'
fig.text(0.75,(0.81-(x0.03775)),round(tm_rate[x],1), size=25, color=c, ha='center', fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.81,0.85,'Attack\nRating', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_rate_a:
if tm_rate_a[x]> 130:
c = 'darkgreen'
elif tm_rate_a[x]> 110:
c = 'mediumseagreen'
elif tm_rate_a[x]> 90:
c = 'black'
elif tm_rate_a[x]> 70:
c = 'indianred'
else:
c = 'darkred'
fig.text(0.81,(0.81-(x0.03775)),round(tm_rate_a[x],1), size=25, color=c, ha='center', fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.84,0.91,'Team Ratings', size=27, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
fig.add_artist(lines.Line2D([0.74, 0.94], [0.905, 0.905], c='grey', linestyle='dotted', linewidth=2, alpha=0.5))
fig.text(0.87,0.85,'Defense\nRating', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_rate_d:
if tm_rate_d[x]< 70:
c = 'darkgreen'
elif tm_rate_d[x]< 90:
c = 'mediumseagreen'
elif tm_rate_d[x]< 110:
c = 'black'
elif tm_rate_d[x]< 130:
c = 'indianred'
else:
c = 'darkred'
fig.text(0.87,(0.81-(x0.03775)),round(tm_rate_d[x],1), size=25, color=c, ha='center', fontproperties=font_normal.prop)
x= x+1
x=0
fig.text(0.93,0.85,'Team\nRank', size=25, color='#000000',va='bottom', ha='center', fontproperties=font_bold.prop)
for i in tm_rate_o:
fig.text(0.93,(0.81-(x*0.03775)),round(tm_rate_o[x],1), size=25, color='#000000', ha='center', fontproperties=font_normal.prop)
x= x+1

Last part is again to add in an explanation, my branding and save:

fig.text(.97,0.03,'@oh_that_crab', size=27, color='#000000', ha='right', style='italic',fontproperties=font_normal.prop)
fig.text(.03,0.03,'For team ratings 100 is League average, for defense lower is better (like goals allowed)\nNumbers above and below are by percent compared to average\nExplanation: https://www.crabstats.xyz/explaining-my-projection-model',
size=17, color='#000000', ha='left',fontproperties=font_normal.prop)
ax_cs_logo = add_image(cs_circ, fig, left=0.065, bottom=0.865, height=0.1)
plt.savefig('Simulations/Premier League Simulation Table - ' + date + '.png', bbox_inches='tight')

That's it. Hope this helps.