Data Science 2: Forza Boogaloo
You’ve read part one of this series and you’ve… well that’s the only prerequisite, really. As before, there are many concepts involved and I’m not even familiar with all of them however I will introduce them as they become relevant.
Table Of Contents
Video killed the Forza Horizon star
Again, these posts are not a flex of my skills. I don’t think I’m particularly good however I have no qualms using data from my race for the purposes of analysis. I took another capture with associated video.
import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.pyplot import figure
# read csv file
ford_falcon = pd.read_csv('/content/drive/MyDrive/ForzaData/ford_falcon_video.csv')
And you can get falcon_video.csv
right here. The video is here in case you’re interested:
146 825 854
. You’ll note that I have the telemetry display up and set to the accelerometer. The numbers are interesting here so let’s just run a plot on only the second lap to see where we’re at.import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.pyplot import figure
import matplotlib.ticker as tick
WELCOME_TO_EARTH = 9.80665
# read csv file
ford_falcon = pd.read_csv('/content/drive/MyDrive/ForzaData/ford_falcon_video.csv')
race_falcon = ford_falcon.loc[(ford_falcon['IsRaceOn'] == 1)]
lap = race_falcon.loc[(race_falcon['Lap'] == 2)]
plt.style.use('default')
fig, ax = plt.subplots()
ax.plot(lap['CurrentLapTime'],lap['AccelerationX'] / WELCOME_TO_EARTH, color="orange", label="Ford Falcon FPV")
ax.set_xlabel("Race Time (M:S)")
ax.set_ylabel("Acceleration X (g)")
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
ax.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
ax.legend()
As you see, there have been some changes.
Style, profile
There’s an interesting line in there, plt.style.use
is where you can access the stylesheets available in matplotlib. I’m pretty OK with default and I’ll be sticking with it for a while however you can absolutely check out a full gallery of style sheets and roll with what works for you. The color for the Ford Falcon is specifically set to orange because the car is orange. No science there but stylesheets will have their own color selection orders and things like that which could make life easier when preparing a number of graphics. The “magic number” of 9.81 also disappeared with a preference for the more precise variable WELCOME_TO_EARTH
.
Subplots of plots
One thing that I didn’t touch upon in the first part was the subplots()
method. There is a concept of subplots in matplotlib that can accomplish various things. This post is digging into the concept presently however it’s worthwhile to note that many examples of matplotlib do not use the subplots method and many others, like this one, use it gratuitously (so far).
import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.pyplot import figure
import matplotlib.ticker as tick
WELCOME_TO_EARTH = 9.80665
# read csv file
ford_falcon = pd.read_csv('/content/drive/MyDrive/ForzaData/ford_falcon_video.csv')
race_falcon = ford_falcon.loc[(ford_falcon['IsRaceOn'] == 1)]
lap = race_falcon.loc[(race_falcon['Lap'] == 2)]
plt.style.use('default')
fig, (ax, ax2)= plt.subplots(2,1)
## first figure
ax.set_title("Acceleration along the X axis")
ax.plot(lap['CurrentLapTime'],lap['AccelerationX'] / WELCOME_TO_EARTH, color="orange", label="Ford Falcon FPV")
ax.set_xlabel("Race Time (M:S)")
ax.set_ylabel("Acceleration X (g)")
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
ax.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
## second figure
ax2.set_title("Acceleration along the Y axis")
ax2.plot(lap['CurrentLapTime'],lap['AccelerationY'] / WELCOME_TO_EARTH, color="blue", label="Ford Falcon FPV")
ax2.set_xlabel("Race Time (M:S)")
ax2.set_ylabel("Acceleration Y (g)")
ax2.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
ax.legend()
ax2.legend()
fig.subplots_adjust(hspace=0.75)
fig.suptitle("Acceleration of the Ford Falcon FPV")
fig.show()
subplots()
returns multiple values as you have seen however that second value can also be multiple values. Pythonic as all get out. You can also do this with a number that will just return an array but, for clarity of examples, I’m going to be using named variables. I’m also at no point passing it an unknown amount of plots that I’d like to make as it would somewhat defeat the point of creating plots with relationships to be analyzed. subplots(2,1)
in this case is going to make two rows and one column.
Axes, bold as love
The reason that the convention for a subplot to be called ax
is that they’re considered to be axes. Plural of axis, to my knowledge, but I’m actually not terribly interested in the reason why and more pleased with the ability to use a Jimi Hendrix reference. fig
is the overarching figure
object and the second parameter can be a number of axes enclosed in parenthesis (they should match the number of cells that will end up being created–I ran into some issues there at one point but I can’t recall exactly what the parameters for error were). At this point ax
and ax2
exist and represent the two different graphs in the overall figure. You can see that for ax2
all of the same things can be set as they are on ax
and this can be completely different data if that’s what is chosen to be displayed. As to why I chose acceleration on x and y, well there is method to this madness. The figure can also have a suptitle
which is, quite literally, a super title.
legend()
of the Hidden Temple
Note that both axes have a call to legend()
right at the end. That shows the, yes, legend that identifies the line. We set that text as label
on the plot()
function call. It’s also extremely Pythonic to have some positional parameters and then just pass others as parameter=value. Pretty wild but it works. Just be aware that you can’t always freestyle those. I’ll admit that I am quite the fan of a legend on a graph but, in this case, it’s been quite gratuitous. Why would we need a legend when we only have the one car’s acceleration being plotted. Can’t they be plotted on the same graph as two separate lines since they share units of measure on the axes? Yes, yes they can.
import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.pyplot import figure
import matplotlib.ticker as tick
WELCOME_TO_EARTH = 9.80665
# read csv file
ford_falcon = pd.read_csv('/content/drive/MyDrive/ForzaData/ford_falcon_video.csv')
race_falcon = ford_falcon.loc[(ford_falcon['IsRaceOn'] == 1)]
lap = race_falcon.loc[(race_falcon['Lap'] == 2)]
plt.style.use('default')
fig, ax = plt.subplots()
## first figure
#ax.set_title("Acceleration along the X axis")
ax.plot(lap['CurrentLapTime'],lap['AccelerationX'] / WELCOME_TO_EARTH, color="orange", label="Acceleration X")
ax.plot(lap['CurrentLapTime'],lap['AccelerationY'] / WELCOME_TO_EARTH, color="blue", label="Acceleration Y")
ax.set_xlabel("Race Time (M:S)")
ax.set_ylabel("Acceleration X (g)")
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
ax.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
ax.legend()
fig.subplots_adjust(hspace=0.75)
fig.suptitle("Acceleration of the Ford Falcon FPV")
fig.show()
Now that legend is really paying off, eh? The Orange Iguanas and the Blue Barracudas teamed up on the same plane makes a graph look cooler and definitely more interesting. There is further meaning to be imparted. Also, the color selections. Again, the style sheet will pick different colors for each line so they don’t need to be specifically called out. But we’re losing a little bit of resolution on the “Acceleration Y” line as the scale for X is much larger. We can do something like point out where the maximum values are and what they are.
Congratulations, now we’re going to make some annotations
This isn’t always necessary in a plot or a graph however it’s sometimes crucial to add some further concrete information to communicate meaning and give some perspective. As a gimmick let’s point out where the highest values are. Matplotlib has annotations which can do just that.
import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.pyplot import figure
import matplotlib.ticker as tick
WELCOME_TO_EARTH = 9.80665
# read csv file
ford_falcon = pd.read_csv('/content/drive/MyDrive/ForzaData/ford_falcon_video.csv')
race_falcon = ford_falcon.loc[(ford_falcon['IsRaceOn'] == 1)]
lap = race_falcon.loc[(race_falcon['Lap'] == 2)]
plt.style.use('default')
fig, ax = plt.subplots()
## first figure
#ax.set_title("Acceleration along the X axis")
ax.plot(lap['CurrentLapTime'],lap['AccelerationX'] / WELCOME_TO_EARTH, color="orange", label="Acceleration X")
ax.plot(lap['CurrentLapTime'],lap['AccelerationY'] / WELCOME_TO_EARTH, color="blue", label="Acceleration Y")
ax.set_xlabel("Race Time (M:S)")
ax.set_ylabel("Acceleration X (g)")
time_max_g_x = lap.loc[(abs(lap['AccelerationX']) == abs(lap['AccelerationX']).max())]
#print(time_max_g_x)
x_note_x = time_max_g_x['CurrentLapTime']
#print(x_note_x)
x_note_y = time_max_g_x['AccelerationX'].mean() / WELCOME_TO_EARTH #i don't know why this doesnt' convert to a value on its own
#print(x_note_y)
time_max_g_y = lap.loc[(abs(lap['AccelerationY']) == abs(lap['AccelerationY']).max())]
y_note_x = time_max_g_y['CurrentLapTime']
y_note_y = time_max_g_y['AccelerationY'].mean() / WELCOME_TO_EARTH
## annotation for max G X
ax.annotate("{:.2f} g".format(x_note_y),
xy=(x_note_x - .1, x_note_y), xycoords='data',
xytext=(-20, -5), textcoords='offset points',
bbox = dict(boxstyle = 'round,pad=0.1', fc = 'orange', alpha = 0.5),
arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'),
horizontalalignment='right', verticalalignment='bottom')
## annotation for max G Y
ax.annotate("{:.2f} g".format(y_note_y),
xy=(y_note_x, y_note_y), xycoords='data',
xytext=(-20, -5), textcoords='offset points',
bbox = dict(boxstyle = 'round,pad=0.1', fc = 'blue', alpha = 0.5),
arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'),
horizontalalignment='right', verticalalignment='bottom')
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
ax.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
ax.legend()
fig.subplots_adjust(hspace=0.75)
fig.suptitle("Acceleration of the Ford Falcon FPV")
fig.show()
You’ll notice the use of abs
to get absolute values. This is important, again, because acceleration is a vector. The sign of the number is the directionality. When watching the video you’ll see that the values in the accelerometer circle are always positive values. Those values are the magnitude of the combined vectors of AccelerationX
and AccelerationY
.
I ran into something odd (at least to me it’s odd) where time_max_g_x
and time_max_g_y
both failed to have their acceleration values be a single value. They were being passed through as a series (even though CurrentLapTime
was not) so I just grabbed the average–can’t mess it up too bad if you use an average–even though the values matched the min()
or max()
values.
We call .annotate()
on the axes that we wish to annotate (in this case just the one) and there are a whole mess of parameters in here–many with parameters of their own. First off is the string that will be our text and again we use that string formatting trick to display a reasonable number. xy
is the X and Y values of your annotation and there can be some variability to this but xycoords=data
tells the annotation to use the coordinates that we are plotting our data on. This makes reasonable sense here. We do a tiny bit of arithmetic to move the point of the arrow just off the point in question. textcoords
assigns the relationship of the coordinates for the text to the coordinates xy
–“offset points” means that xytext
is the offset from that point versus being absolute or a data coordinate or something. bbox
is the little box around the text and has it’s own set of properties in there that are interesting–feel free to mess with those. Same with arrowprops
.
Magnitude: Pop, pop!
This plot is cute and all but it isn’t relating to what we’re seeing in the video. That always positive number is the magnitude of the two vectors (acceleration x and acceleration y) and that’s just the distance from (0,0). We can readily compute the magnitude and add it to our dataframe. Cool, right? No? I guess it’s just me, then, huh?
import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.pyplot import figure
import matplotlib.ticker as tick
import numpy as np # new import
WELCOME_TO_EARTH = 9.80665
# read csv file
ford_falcon = pd.read_csv('/content/drive/MyDrive/ForzaData/ford_falcon_video.csv')
race_falcon = ford_falcon.loc[(ford_falcon['IsRaceOn'] == 1)]
lap = race_falcon.loc[(race_falcon['Lap'] == 2)]
## Add total acceleration to dataframe
#lap['TotalAcceleration'] = np.sqrt(pow(lap['AccelerationX'],2) + pow(lap['AccelerationY'],2) + pow(lap['AccelerationZ'],2))/ 9.81
lap = lap.assign(TotalAcceleration=lambda x: np.sqrt(pow(x['AccelerationX'],2) + pow(x['AccelerationY'],2))/ WELCOME_TO_EARTH)
plt.style.use('default')
fig, ax = plt.subplots()
ax.plot(lap['CurrentLapTime'],lap['TotalAcceleration'], color="orange", label="Acceleration X + Y")
ax.set_xlabel("Race Time (M:S)")
ax.set_ylabel("Acceleration X (g)")
time_max_g = lap.loc[(abs(lap['TotalAcceleration']) == abs(lap['TotalAcceleration']).max())]
#print(time_max_g_x)
total_note_x = time_max_g['CurrentLapTime']
#print(x_note_x)
total_note_y = time_max_g['TotalAcceleration'].mean() #i don't know why this doesnt' convert to a value on its own
#print(x_note_y)
## annotation for max G X
ax.annotate("{:.2f} g".format(total_note_y),
xy=(total_note_x - .1, total_note_y), xycoords='data',
xytext=(-20, -5), textcoords='offset points',
bbox = dict(boxstyle = 'round,pad=0.1', fc = 'orange', alpha = 0.5),
arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'),
horizontalalignment='right', verticalalignment='bottom')
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
ax.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
ax.legend()
fig.subplots_adjust(hspace=0.75)
fig.suptitle("Total Acceleration of the Ford Falcon FPV")
fig.show()
lambda
is doing the heavy lift here. I’m honestly unsure why it’s called a lambda function but it’s basically operating on this
where this
is an object-oriented concept. It’s also iterating through the list. It’s very pithy. Not even a line of code to go through all the data and perform that operation. I’ll look that up eventually. Either way, that’s the recommended way to add a column to a dataframe and the important part is TotalAcceleration = lambda: x
. You can also apply operations to all of the Acceleration columns to turn them in g units right now (but we won’t). TotalAcceleration
is already in g, however, and mixing units is not advisable but we’ve gone too far. The great part of this is that the rest of the plot remains mostly unchanged. The notation code is potentially generic enough that it can be refactored out into a function with optional overrides for the location and color of the box.
Message
There’s a lot that you can do here but it isn’t always just about the plotting and the programming but the intersection of that with the information. We’ve adjusted and altered our plots a few times to get to a/my goal of visualizing this information. I’m primarily concerned with understanding the magnitude of the forces. How we present that is important in terms of communicating something specific. There are yet more ways to go about it and we will indeed go about it on some other ways in the next post.
./content/posts/data-science-2-forza-boogaloo.md 9:103 warning In general, don't use an Microsoft.Ellipses ellipsis. 9:142 warning Consider removing 'really'. Microsoft.Adverbs 9:200 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 9:227 warning Consider using 'all' instead Microsoft.Wordiness of 'all of'. 9:246 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 14:1 error Use 'aren't' instead of 'are Microsoft.Contractions not'. 14:40 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 14:65 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 14:94 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 14:128 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 14:165 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 26:109 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 26:208 warning Try to avoid using Microsoft.We first-person plural like 'let's'. 26:266 warning Try to avoid using Microsoft.We first-person plural like 'we'. 51:111 warning Try to avoid using Microsoft.We first-person plural like 'We'. 56:120 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 59:15 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 59:354 error Use 'don't' instead of 'do Microsoft.Contractions not'. 100:231 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 100:270 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 103:122 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 103:140 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 103:157 warning Consider removing 'terribly'. Microsoft.Adverbs 103:492 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 103:677 warning Consider using 'all' instead Microsoft.Wordiness of 'all of'. 103:714 error Use 'they're' instead of 'they Microsoft.Contractions are'. 103:783 error Use 'what's' instead of 'what Microsoft.Contractions is'. 103:824 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 106:119 warning Try to avoid using Microsoft.We first-person plural like 'We'. 106:188 warning Consider removing 'extremely'. Microsoft.Adverbs 106:382 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 106:482 warning Try to avoid using Microsoft.We first-person plural like 'we'. 106:504 warning Try to avoid using Microsoft.We first-person plural like 'we'. 139:20 warning Consider removing 'really'. Microsoft.Adverbs 139:360 warning Try to avoid using Microsoft.We first-person plural like 'we'. 139:464 warning Try to avoid using Microsoft.We first-person plural like 'We'. 139:537 error Use 'they're' instead of 'they Microsoft.Contractions are'. 141:25 warning Try to avoid using Microsoft.We first-person plural like 'we'. 142:185 warning Try to avoid using Microsoft.We first-person plural like 'let's'. 206:1 warning Use first person (such as 'I Microsoft.FirstPerson ') sparingly. 206:39 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 206:230 error Use 'wasn't' instead of 'was Microsoft.Contractions not'. 206:241 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 208:1 warning Try to avoid using Microsoft.We first-person plural like 'We'. 208:40 warning Try to avoid using Microsoft.We first-person plural like 'we'. 208:210 warning Try to avoid using Microsoft.We first-person plural like 'our'. 208:229 warning Try to avoid using Microsoft.We first-person plural like 'we'. 208:454 warning Try to avoid using Microsoft.We first-person plural like 'we'. 208:454 error Use 'we're' instead of 'we Microsoft.Contractions are'. 208:470 warning Try to avoid using Microsoft.We first-person plural like 'our'. 208:517 warning Try to avoid using Microsoft.We first-person plural like 'We'. 210:21 warning Don't use end punctuation in Microsoft.HeadingPunctuation headings. 211:57 warning Try to avoid using Microsoft.We first-person plural like 'we'. 211:225 warning Try to avoid using Microsoft.We first-person plural like 'We'. 211:232 warning Consider removing 'readily'. Microsoft.Adverbs 211:276 warning Try to avoid using Microsoft.We first-person plural like 'our'. 211:307 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 211:326 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 267:40 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 267:44 warning Consider removing 'honestly'. Microsoft.Adverbs 267:220 warning Consider removing 'very'. Microsoft.Adverbs 267:503 warning Consider using 'all' instead Microsoft.Wordiness of 'all of'. 267:574 warning Try to avoid using Microsoft.We first-person plural like 'we'. 267:648 error Use 'isn't' instead of 'is Microsoft.Contractions not'. 267:669 warning Try to avoid using Microsoft.We first-person plural like 'we'. 267:788 warning Consider removing Microsoft.Adverbs 'potentially'. 270:153 warning Try to avoid using Microsoft.We first-person plural like 'We'. 270:180 warning Try to avoid using Microsoft.We first-person plural like 'our'. 270:214 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 270:255 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 270:331 warning Try to avoid using Microsoft.We first-person plural like 'we'. 270:342 error Use 'that's' instead of 'that Microsoft.Contractions is'. 270:449 warning Try to avoid using Microsoft.We first-person plural like 'we'. 272:210 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly.✖ 9 errors, 65 warnings and 0 suggestions in 1 file.